深入Vite核心编译能力

news2024/11/23 12:53:03

我们知道,Vite 在开发阶段实现了一个按需加载的服务器,每一个文件请求进来都会经历一系列的编译流程,然后 Vite 会将编译结果响应给浏览器。而在生产环境下,Vite 同样会执行一系列编译过程,将编译结果交给 Rollup 进行模块打包。这一系列的编译过程指的就是 Vite 的插件工作流水线(Pipeline),那它是如何实现的呢?

一、插件容器

通过前文的介绍《Vite 是如何站在巨人的肩膀上实现的》我们知道 Vite 的插件机制是与 Rollup 兼容的,但它在开发和生产环境下的实现稍有差别,你可以回顾一下这张架构图。

在这里插入图片描述

可以看到,在开发环境中,Vite 模拟了 Rollup 的插件机制,设计了一个PluginContainer 对象来调度各个插件;在生产环境中 Vite 直接调用 Rollup 进行打包,所以 Rollup 可以调度各种插件。

所以,在开发环境中,PluginContainer就是非常重要的。事实上,PluginContainer 的 实现 基于借鉴于 WMR 中的rollup-plugin-container.js,主要分为 2 个部分:

  • 实现 Rollup 插件钩子的调度
  • 实现插件钩子内部的 Context 上下文对象

首先,我们可以通过 container 的定义 来看看各个 Rollup 钩子的实现方式,以下是精简后的代码:

const container = {
  // 异步串行钩子
  options: await (async () => {
    let options = rollupOptions
    for (const plugin of plugins) {
      if (!plugin.options) continue
      options =
        (await plugin.options.call(minimalContext, options)) || options
    }
    return options;
  })(),
  // 异步并行钩子
  async buildStart() {
    await Promise.all(
      plugins.map((plugin) => {
        if (plugin.buildStart) {
          return plugin.buildStart.call(
            new Context(plugin) as any,
            container.options as NormalizedInputOptions
          )
        }
      })
    )
  },
  // 异步优先钩子
  async resolveId(rawId, importer) {
    // 上下文对象,后文介绍
    const ctx = new Context()


    let id: string | null = null
    const partial: Partial<PartialResolvedId> = {}
    for (const plugin of plugins) {
      const result = await plugin.resolveId.call(
        ctx as any,
        rawId,
        importer,
        { ssr }
      )
      if (!result) continue;
      return result;
    }
  }
  // 异步优先钩子
  async load(id, options) {
    const ctx = new Context()
    for (const plugin of plugins) {
      const result = await plugin.load.call(ctx as any, id, { ssr })
      if (result != null) {
        return result
      }
    }
    return null
  },
  // 异步串行钩子
  async transform(code, id, options) {
    const ssr = options?.ssr
    // 每次 transform 调度过程会有专门的上下文对象,用于合并 SourceMap,后文会介绍
    const ctx = new TransformContext(id, code, inMap as SourceMap)
    ctx.ssr = !!ssr
    for (const plugin of plugins) {
      let result: TransformResult | string | undefined
      try {
        result = await plugin.transform.call(ctx as any, code, id, { ssr })
      } catch (e) {
        ctx.error(e)
      }
      if (!result) continue;
      // 省略 SourceMap 合并的逻辑 
      code = result;
    }
    return {
      code,
      map: ctx._getCombinedSourcemap()
    }
  },
  // close 钩子实现省略
}

在之前的文章《深入理解 Rollup 的插件机制》 中我们已经系统学习过 Rollup 中异步、串行、并行等钩子类型的执行原理。现在再来看PluginContainer的原理就很简单了。

不过值得注意的是,在各种钩子被调用的时候,Vite 会强制将钩子函数的 this 绑定为一个上下文对象:

const ctx = new Context()
const result = await plugin.load.call(ctx as any, id, { ssr })

我们知道,在 Rollup 钩子函数中,我们可以调用this.emitFile、this.resolve 等诸多的上下文方法。因此,Vite 除了要模拟各个插件的执行流程,还需要模拟插件执行的上下文对象,代码中的 Context 对象就是用来完成这件事情的。我们来看看 Context 对象的具体实现:

import { RollupPluginContext } from 'rollup';
type PluginContext = Omit<
  RollupPluginContext,
  // not documented
  | 'cache'
  // deprecated
  | 'emitAsset'
  | 'emitChunk'
  | 'getAssetFileName'
  | 'getChunkFileName'
  | 'isExternal'
  | 'moduleIds'
  | 'resolveId'
  | 'load'
>


const watchFiles = new Set<string>()


class Context implements PluginContext {
  // 实现各种上下文方法
  // 解析模块 AST(调用 acorn)
  parse(code: string, opts: any = {}) {
    return parser.parse(code, {
      sourceType: 'module',
      ecmaVersion: 'latest',
      locations: true,
      ...opts
    })
  }
  // 解析模块路径
  async resolve(
    id: string,
    importer?: string,
    options?: { skipSelf?: boolean }
  ) {
    let skip: Set<Plugin> | undefined
    if (options?.skipSelf && this._activePlugin) {
      skip = new Set(this._resolveSkips)
      skip.add(this._activePlugin)
    }
    let out = await container.resolveId(id, importer, { skip, ssr: this.ssr })
    if (typeof out === 'string') out = { id: out }
    return out as ResolvedId | null
  }


  // 以下两个方法均从 Vite 的模块依赖图中获取相关的信息
  // 我们将在下一节详细介绍模块依赖图,本节不做展开
  getModuleInfo(id: string) {
    return getModuleInfo(id)
  }


  getModuleIds() {
    return moduleGraph
      ? moduleGraph.idToModuleMap.keys()
      : Array.prototype[Symbol.iterator]()
  }
  
  // 记录开发阶段 watch 的文件
  addWatchFile(id: string) {
    watchFiles.add(id)
    ;(this._addedImports || (this._addedImports = new Set())).add(id)
    if (watcher) ensureWatchedFile(watcher, id, root)
  }


  getWatchFiles() {
    return [...watchFiles]
  }
  
  warn() {
    // 打印 warning 信息
  }
  
  error() {
    // 打印 error 信息
  }
  
  // 其它方法只是声明,并没有具体实现,这里就省略了
}

很显然,Vite 将 Rollup 的PluginContext对象重新实现了一遍,因为只是开发阶段用到,所以去除了一些打包相关的方法实现。同时,上下文对象与 Vite 开发阶段的 ModuleGraph 即模块依赖图相结合,是为了实现开发时的 HMR。

另外,transform 钩子也会绑定一个插件上下文对象,不过这个对象和其它钩子不同,精简的实现代码如下:

class TransformContext extends Context {
  constructor(filename: string, code: string, inMap?: SourceMap | string) {
    super()
    this.filename = filename
    this.originalCode = code
    if (inMap) {
      this.sourcemapChain.push(inMap)
    }
  }


  _getCombinedSourcemap(createIfNull = false) {
    return this.combinedMap
  }


  getCombinedSourcemap() {
    return this._getCombinedSourcemap(true) as SourceMap
  }
}

可以看到,TransformContext继承自之前所说的Context对象,也就是说 transform 钩子的上下文对象相比其它钩子只是做了一些扩展,增加了 sourcemap 合并的功能,将不同插件的 transform 钩子执行后返回的 sourcemap 进行合并,以保证 sourcemap 的准确性和完整性。

二、插件工作流

接下来,让我们把目光集中在resolvePlugins的实现上,Vite 所有的插件就是在这里被收集起来的,具体实现如下:

export async function resolvePlugins(
  config: ResolvedConfig,
  prePlugins: Plugin[],
  normalPlugins: Plugin[],
  postPlugins: Plugin[]
): Promise<Plugin[]> {
  const isBuild = config.command === 'build'
  // 收集生产环境构建的插件,后文会介绍
  const buildPlugins = isBuild
    ? (await import('../build')).resolveBuildPlugins(config)
    : { pre: [], post: [] }


  return [
    // 1. 别名插件
    isBuild ? null : preAliasPlugin(),
    aliasPlugin({ entries: config.resolve.alias }),
    // 2. 用户自定义 pre 插件(带有`enforce: "pre"`属性)
    ...prePlugins,
    // 3. Vite 核心构建插件
    // 数量比较多,暂时省略代码
    // 4. 用户插件(不带有 `enforce` 属性)
    ...normalPlugins,
    // 5. Vite 生产环境插件 & 用户插件(带有 `enforce: "post"`属性)
    definePlugin(config),
    cssPostPlugin(config),
    ...buildPlugins.pre,
    ...postPlugins,
    ...buildPlugins.post,
    // 6. 一些开发阶段特有的插件
    ...(isBuild
      ? []
      : [clientInjectionsPlugin(config), importAnalysisPlugin(config)])
  ].filter(Boolean) as Plugin[]
}

从上述代码中我们可以总结出 Vite 插件的具体执行顺序:

  • 别名插件包括 vite:pre-alias和@rollup/plugin-alias,用于路径别名替换。
  • 用户自定义 pre 插件,也就是带有enforce: "pre"属性的自定义插件。
  • Vite 核心构建插件,这部分插件为 Vite 的核心编译插件,数量比较多,我们在下部分一一拆解。
  • 用户自定义的普通插件,即不带有 enforce 属性的自定义插件。
  • Vite 生产环境插件和用户插件中带有enforce: "post"属性的插件。
  • 一些开发阶段特有的插件,包括环境变量注入插件clientInjectionsPlugin和 import 语句分析及重写插件importAnalysisPlugin。

那么,在执行过程中 Vite 到底应用了哪些插件,以及这些插件内部究竟做了什么?接下来,我们一一梳理一下。

三、插件功能梳理

除用户自定义插件之外,我们需要梳理的 Vite 内置插件有下面这几类:

  • 别名插件
  • 核心构建插件
  • 生产环境特有插件
  • 开发环境特有插件

3.1 别名插件

别名插件有两个,分别是 vite:pre-alias 和 @rollup/plugin-alias。 前者主要是为了将 bare import 路径重定向到预构建依赖的路径,比如:

// 假设 React 已经过 Vite 预构建
import React from 'react';
// 会被重定向到预构建产物的路径
import React from '/node_modules/.vite/react.js'

后者则是实现了比较通用的路径别名(即resolve.alias配置)的功能,使用的是Rollup 官方 Alias 插件。

3.2 核心构建插件

3.2.1 module preload 特性的 Polyfill

当我们在Vite 配置文件中开启如下配置时,Vite 会自动应用 modulePreloadPolyfillPlugin 插件:

{
  build: {
    polyfillModulePreload: true
  }
}

在上面的配置中,其作用就是注入 module preload 的 Polyfill 代码,具体实现 摘自之前我们提到过的 es-module-shims这个库,实现原理如下:

  • 扫描出当前所有的 modulepreload 标签,拿到 link 标签对应的地址,通过执行 fetch 实现预加载;
  • 同时通过 MutationObserver 监听 DOM 的变化,一旦发现包含 modulepreload 属性的 link 标签,则同样通过 fetch 请求实现预加载。

需要注意的是,由于部分支持原生 ESM 的浏览器并不支持 module preload,因此某些情况下需要注入相应的 polyfill 进行降级。

3.2.2 路径解析插件

路径解析插件(即vite:resolve)是 Vite 中比较核心的插件,几乎所有重要的 Vite 特性都离不开这个插件的实现,诸如依赖预构建、HMR、SSR 等等。同时它也是实现相当复杂的插件,一方面实现了Node.js 官方的 resolve 算法,另一方面需要支持前面所说的各项特性,可以说是专门给 Vite 实现了一套路径解析算法。

3.2.3 内联脚本加载插件

对于 HTML 中的内联脚本,Vite 会通过vite:html-inline-script-proxy 插件来进行加载。比如下面这个 script 标签:

<script type="module">
import React from 'react';
console.log(React)
</script>

这些内容会在后续的build-html插件从 HTML 代码中剔除,并且变成下面的这一行代码插入到项目入口模块的代码中,如下。

import '/User/xxx/vite-app/index.html?http-proxy&index=0.js'

而 vite:html-inline-script-proxy 就是用来加载这样的模块,实现如下:

const htmlProxyRE = /?html-proxy&index=(\d+).js$/


export function htmlInlineScriptProxyPlugin(config: ResolvedConfig): Plugin {
  return {
    name: 'vite:html-inline-script-proxy',
    load(id) {
      const proxyMatch = id.match(htmlProxyRE)
      if (proxyMatch) {
        const index = Number(proxyMatch[1])
        const file = cleanUrl(id)
        const url = file.replace(normalizePath(config.root), '')
        // 内联脚本的内容会被记录在 htmlProxyMap 这个表中
        const result = htmlProxyMap.get(config)!.get(url)![index]
        if (typeof result === 'string') {
          // 加载脚本的具体内容
          return result
        } else {
          throw new Error(`No matching HTML proxy module found from ${id}`)
        }
      }
    }
  }
}

3.2.4 CSS 编译插件

即名为vite:css的插件,主要实现下面这些功能:

  • CSS 预处理器的编译
  • CSS Modules
  • Postcss 编译
  • 通过 @import 记录依赖,便于 HMR

3.2.5 Esbuild插件

即vite:esbuild的插件,用来进行 .js、.ts、.jsx和tsx,代替了传统的 Babel 或者 TSC 的功能,这也是 Vite 开发阶段性能强悍的一个原因。插件中主要的逻辑是transformWithEsbuild函数,顾名思义,你可以通过这个函数进行代码转译。

当然,Vite 本身也导出了这个函数,作为一种通用的 transform 能力,我们可以采用下面的方式来使用它:

import { transformWithEsbuild } from 'vite';


// 传入两个参数: code, filename
transformWithEsbuild('<h1>hello</h1>', './index.tsx').then(res => {
  // {
  //   warnings: [],
  //   code: '/* @__PURE__ */ React.createElement("h1", null, "hello");\n',
  //   map: {/* sourcemap 信息 */}
  // }
  console.log(res);
})

3.2.6 静态资源加载插件

静态资源加载插件包括如下几个:

  • vite:json 用来加载 JSON 文件,通过@rollup/pluginutils的dataToEsm方法可实现 JSON 的按名导入,具体实现见json链接;
  • vite:wasm 用来加载 .wasm 格式的文件,具体实现见wasm链接;
  • vite:worker 用来 Web Worker 脚本,插件内部会使用 Rollup 对 Worker 脚本进行打包,具体实现见works链接;
  • vite:asset,开发阶段实现了其他格式静态资源的加载,而生产环境会通过 renderChunk 钩子将静态资源地址重写为产物的文件地址,如./img.png 重写为 https://cdn.xxx.com/assets/img.91ee297e.png。

值得注意的是,Rollup 本身存在 asset cascade 问题,即静态资源哈希更新,引用它的 JS 的哈希并没有更新(issue 链接)。因此 Vite 在静态资源处理的时候,并没有交给 Rollup 生成资源哈希,而是自己根据资源内容生成哈希,并手动进行路径重写,以此避免 asset-cascade 问题。

3.3生产环境插件

3.3.1 全局变量替换插件

提供全局变量替换功能,添加如下的配置。

// vite.config.ts
const version = '2.0.0';


export default {
  define: {
    __APP_VERSION__: `JSON.stringify(${version})`
  }
}

全局变量替换的功能和我们之前提到的@rollup/plugin-replace 插件的功能差不多,当然在实现上 Vite 会有所区别,主要体现在如下两点:

  • 开发环境下,Vite 会通过将所有的全局变量挂载到window对象,而不用经过 define 插件的处理,节省编译开销;
  • 生产环境下,Vite 会使用 define 插件,进行字符串替换以及 sourcemap 生成。

特殊情况下,SSR 构建会在开发环境经过这个插件,仅替换字符串。

3.3.2 CSS 后处理插件

CSS 后处理插件即name为vite:css-post的插件,它的功能包括开发阶段 CSS 响应结果处理和生产环境 CSS 文件生成。

首先,在开发阶段,这个插件会将之前的 CSS 编译插件处理后的结果,包装成一个 ESM 模块,返回给浏览器,点击查看实现代码。

其次,生产环境中,Vite 默认会通过这个插件进行 CSS 的 code splitting,即对于每个异步 chunk,Vite 会将其依赖的 CSS 代码单独打包成一个文件,关键代码如下(源码链接):

const fileHandle = this.emitFile({
  name: chunk.name + '.css',
  type: 'asset',
  source: chunkCSS
});

如果 CSS 的 code splitting 功能被关闭(通过build.cssCodeSplit配置),那么 Vite 会将所有的 CSS 代码打包到同一个 CSS 文件中,点击查看实现。最后,插件会调用 Esbuild 对 CSS 进行压缩,实现在 minifyCSS 函数中,点击查看实现。

3.3.3 HTML 构建插件

HTML 构建插件 即build-html插件。之前我们在内联脚本加载插件中提到过,项目根目录下的html会转换为一段 JavaScript 代码,如下所示:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  // 普通方式引入
  <script src="./index.ts"></script>
  // 内联脚本
  <script type="module">
    import React from 'react';
    console.log(React)
  </script>
</body>
</html>

首先,当 Vite 在生产环境transform这段入口 HTML 时,会做 3 件事情:

  • 对 HTML 执行各个插件中带有 enforce: “pre” 属性的 transformIndexHtml 钩子;
  • 将其中的 script 标签内容删除,并将其转换为 import 语句如import ‘./index.ts’,并记录下来;
  • 在 transform 钩子中返回记录下来的 import 内容,将 import 语句作为模块内容进行加载。

也就是说,虽然 Vite 处理的是一个 HTML 文件,但最后进行打包的内容却是一段 JS 的内容,代码简化后如下所示:

export function buildHtmlPlugin() {
  name: 'vite:build',
  transform(html, id) {
    if (id.endsWith('.html')) {
      let js = '';
      // 省略 HTML AST 遍历过程(通过 @vue/compiler-dom 实现)
      // 收集 script 标签,转换成 import 语句,拼接到 js 字符串中
      return js;
    }
  }
}

其次,在生成产物的最后一步即generateBundle钩子中,拿到入口 Chunk,分析入口 Chunk 的内容, 然后分情况进行处理。

如果只有 import 语句,先通过 Rollup 提供的 chunk 和 bundle 对象获取入口 chunk 所有的依赖 chunk,并将这些 chunk 进行后序排列,如 a 依赖 b,b 依赖 c,最后的依赖数组就是[c, b, a]。然后依次将 c,b, a 生成三个 script 标签,插入 HTML 中。最后,Vite 会将入口 chunk 的内容从 bundle 产物中移除,因此它的内容只要 import 语句,而它 import 的 chunk 已经作为 script 标签插入到了 HTML 中,那入口 Chunk 的存在也就没有意义了。

如果除了 import 语句,还有其它内容, Vite 就会将入口 Chunk 单独生成一个 script 标签,分析出依赖的后序排列(和上一种情况分析手段一样),然后通过注入 标签对入口文件的依赖 chunk 进行预加载。

最后,插件会调用用户插件中带有 enforce: “post” 属性的 transformIndexHtml 钩子,对 HTML 进行进一步的处理,源码链接。

3.3.4 Commonjs 转换插件

我们知道,在开发环境中,Vite 使用 Esbuild 将 Commonjs 转换为 ESM,而生产环境中,Vite 会直接使用 Rollup 的官方插件 @rollup/plugin-commonjs。

3.3.5 date-uri 插件

date-uri 插件用来支持 import 模块中含有 Base64 编码的情况,使用方式如下:

import batman from 'data:application/json;base64, eyAiYmF0bWFuIjogInRydWUiIH0=';

相关的实现源码链接如下:https://github.com/vitejs/vite/blob/2b7e836f84b56b5f3dc81e0f5f161a9b5f9154c0/packages/vite/src/node/plugins/dataUri.ts#L14

3.3.6 dynamic-import-vars 插件

用于支持在动态 import 中使用变量的功能,如下示例代码:

function importLocale(locale) {
  return import(`./locales/${locale}.js`);
}

内部使用的是 Rollup 的官方插件@rollup/plugin-dynamic-import-vars。

3.3.7 import-meta-url 插件

用来转换如下格式的资源 URL,使用方法如下:

new URL('./foo.png', import.meta.url)

将其转换为生产环境的 URL 格式,如下所示:

// 使用 self.location 来保证低版本浏览器和 Web Worker 环境的兼容性
new URL('./assets.a4b3d56d.png, self.location)

当然,对于动态 import 的情况也能进行支持,如下面的这种写法:

function getImageUrl(name) {
  return new URL(`./dir/${name}.png`, import.meta.url).href
}

Vite 识别到./dir/${name}.png这样的模板字符串,会将整行代码转换成下面这样:

function getImageUrl(name) {
    return import.meta.globEager('./dir/**.png')[`./dir/${name}.png`].default;
}

击查看具体实现

3.3.8 生产环境 import 分析插件

vite:build-import-analysis 插件会在生产环境打包时用作 import 语句分析和重写,主要目的是对动态 import 的模块进行预加载处理。

对含有动态 import 的 chunk 而言,会在插件的tranform钩子中被添加这样一段工具代码用来进行模块预加载,逻辑并不复杂,你可以参考源码实现。关键代码简化后如下:

function preload(importModule, deps) {
  return Promise.all(
    deps.map(dep => {
      // 如果异步模块的依赖还没有加载
      if (!alreadyLoaded(dep)) { 
        // 创建 link 标签加载,包括 JS 或者 CSS
        document.head.appendChild(createLink(dep))  
        // 如果是 CSS,进行特殊处理,后文会介绍
        if (isCss(dep)) {
          return new Promise((resolve, reject) => {
            link.addEventListener('load', resolve)
            link.addEventListener('error', reject)
          })
        }
      }
    })
  ).then(() => importModule())
}

我们知道,Vite 内置了 CSS 代码分割的能力,当一个模块通过动态 import 引入的时候,这个模块会被单独打包成一个 chunk,与此同时这个模块中的样式代码也会打包成单独的 CSS 文件。如果异步模块的 CSS 和 JS 同时进行预加载,那么在某些浏览器下(如 IE)就会出现 FOUC 问题,页面样式会闪烁,影响用户体验。但 Vite 通过监听 link 标签 load 事件的方式来保证 CSS 在 JS 之前加载完成,从而解决了 FOUC 问题。你可以注意下面这段关键代码:

if (isCss) {
  return new Promise((res, rej) => {
    link.addEventListener('load', res)
    link.addEventListener('error', rej)
  })
}

现在,我们已经知道了预加载的实现方法,那么 Vite 是如何将动态 import 编译成预加载的代码的呢?从源码的transform钩子实现中,不难发现 Vite 会将动态 import 的代码进行转换,如下代码所示:

// 转换前
import('a')
// 转换后
__vitePreload(() => 'a', __VITE_IS_MODERN__ ?"__VITE_PRELOAD__":void)

其中,__vitePreload 会被加载为前文中的 preload 工具函数,VITE_IS_MODERN 会在 renderChunk 中被替换成 true 或者 false,表示是否为 Modern 模式打包,而对于"VITE_PRELOAD",Vite 会在 generateBundle 阶段,分析出 a 模块所有依赖文件(包括 CSS),将依赖文件名的数组作为 preload 工具函数的第二个参数。

同时,对于 Vite 独有的 import.meta.glob 语法,也会在这个插件中进行编译,如:

const modules = import.meta.glob('./dir/*.js')

上面的代码会通过插件转换成下面这段代码:

const modules = {
  './dir/foo.js': () => import('./dir/foo.js'),
  './dir/bar.js': () => import('./dir/bar.js')
}

具体的实现在 transformImportGlob 函数中,除了被该插件使用外,这个函数被还依赖预构建、开发环境 import 分析等核心流程使用,属于一类比较底层的逻辑。

3.3.9 JS 压缩插件

Vite 中提供了两种 JS 代码压缩的工具,即 Esbuild 和 Terser,分别由两个插件插件实现:

  • vite:esbuild-transpile:在 renderChunk 阶段,调用 Esbuild 的 transform API,并指定 minify 参数,从而实现 JS 的压缩。
  • vite:terser:同样也在 renderChunk 阶段,Vite 会单独的 Worker 进程中调用 Terser 进行 JS 代码压缩。

3.3.10 构建报告插件

输出构建报告主要用到三个插件:vite:manifest、vite:ssr-manifest和vite:reporter。

  • vite:manifest:提供打包后的各种资源文件及其关联信息,如下内容所示:
// manifest.json
{
  "index.html": {
    "file": "assets/index.8edffa56.js",
    "src": "index.html",
    "isEntry": true,
    "imports": [
      // JS 引用
      "_vendor.71e8fac3.js"
    ],
    "css": [
      // 样式文件应用
      "assets/index.458f9883.css"
    ],
    "assets": [
      // 静态资源引用
      "assets/img.9f0de7da.png"
    ]
  },
  "_vendor.71e8fac3.js": {
    "file": "assets/vendor.71e8fac3.js"
  }
}
  • vite:ssr-manifest:提供每个模块与 chunk 之间的映射关系,方便 SSR 时期通过渲染的组件来确定哪些 chunk 会被使用,从而按需进行预加载。最后插件输出的内容如下:
// ssr-manifest.json
{
  "node_modules/object-assign/index.js": [
    "/assets/vendor.71e8fac3.js"
  ],
  "node_modules/object-assign/index.js?commonjs-proxy": [
    "/assets/vendor.71e8fac3.js"
  ],
  // 省略其它模块信息
}
  • vite:reporter:主要提供打包时的命令行构建日志:
    在这里插入图片描述

3.4 开发环境插件

3.4.1 环境变量注入插件

在开发环境中,Vite 会自动往 HTML 中注入一段 client 的脚本:

<script type="module" src="/@vite/client"></script>

这段脚本主要提供注入环境变量、处理 HMR 更新逻辑、构建出现错误时提供报错界面等功能,而我们这里要介绍的vite:client-inject就是来完成时环境变量的注入,将 client 脚本中的__MODE__、BASE、__DEFINE__等等字符串替换为运行时的变量,实现环境变量以及 HMR 相关上下文信息的注入,代码链接。

3.4.2 import 分析插件

Vite 会在开发阶段加入 import 分析插件,即vite:import-analysis,与之前所介绍的vite:build-import-analysis相对应,主要处理 import 语句相关的解析和重写,但vite:import-analysis 插件的关注点会不太一样,主要围绕 Vite 开发阶段的各项特性来实现,我们可以来梳理一下这个插件需要做哪些事情:

  • 对 bare import,将路径名转换为真实的文件路径,如:
// 转换前
import 'foo'
// 转换后
// tip: 如果是预构建的依赖,则会转换为预构建产物的路径
import '/@fs/project/node_modules/foo/dist/foo.js'

此代码主要调用 PluginContainer的上下文对象方法即this.resolve实现,这个方法会调用所有插件的 resolveId 方法,包括之前介绍的vite:pre-alias和vite:resolve,完成路径解析的核心逻辑。

  • 对于 HMR 的客户端 API,即 import.meta.hot,Vite 在识别到这样的 import 语句后,一方面会注入 import.meta.hot 的实现,因为浏览器原生并不具备这样的 API;另一方面会识别 accept 方法,并判断 accept 是否为接受自身更新的类型。如果是,则标记为上isSelfAccepting 的 flag,便于 HMR 在服务端进行更新时进行HMR Boundary的查找。
  • 对于全局环境变量读取语句,即 import.meta.env,Vite 会注入 import.meta.env 的实现,也就是如下的env字符串。
 // config 即解析完的配置
let env = `import.meta.env = ${JSON.stringify({
  ...config.env,
  SSR: !!ssr
})};`
// 对用户配置的 define 对象中,将带有 import.meta.env 前缀的全局变量挂到 import.meta.env 对象上
for (const key in config.define) {
  if (key.startsWith(`import.meta.env.`)) {
    const val = config.define[key]
    env += `${key} = ${
      typeof val === 'string' ? val : JSON.stringify(val)
    };`
  }
}
  • 对于import.meta.glob语法,Vite 同样会调用之前提到的transformImportGlob 函数来进行语法转换,但与生产环境的处理不同,在转换之后,Vite 会将该模块通过 glob 导入的依赖模块记录在 server 实例上,以便于 HMR 更新的时候能得到更准确的模块依赖信息。

四、小结

本文主要围绕PluginContainer 的实现机制和 Vite 内置插件各自的作用进行讲解。首先,PluginContainer 主要由两部分实现,包括 Rollup 插件钩子的调度和插件钩子内部的 Context 上下文对象实现,总体上模拟了 Rollup 的插件机制。其实,我们介绍了Vite 内置的插件,包括四大类: 别名插件、核心构建插件、生产环境特有插件和开发环境特有插件。

此外,在学习这些插件的过程中,我们切忌扎到众多繁琐的实现细节中,要尽可能抓关键的实现思路,来高效理解插件背后的原理,这样学习效率会更高。进一步来讲,在你理解了各个插件的实现原理之后,如果遇到某些场景下需要调试某些插件的代码,你也可以做到有的放矢。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/748390.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

CP AUTOSAR中的CanTrcv

环境 CanTrcv驱动实际上是要实现CanIf指出的接口&#xff0c;包括5个API函数 CanTrcv_SetOpMode CanTrcv_GetOpMode CanTrcv_GetBusWuReason CanTrcv_SetWakeupMode CanTrcv_CheckWakeup CanIf接口会有对应的接口回调CanTrcv的API&#xff0c;包括5个A…

CODESYS 联合体变量(Union)

联合Union也是一种特殊的自定义类型这种类型定义的变量也包含一系列的成员&#xff0c;特征是这些成员公用同一块空间&#xff08;所以联合也叫共用体&#xff09;&#xff0c;联合的成员是共用同一块内存空间的&#xff0c;这样一个联合变量的大小&#xff0c;至少是最大成员的…

Drools用户手册翻译——第三章 构建,部署,应用和运行(三)运行

主要对于运行相关内容的介绍&#xff0c;又多了很多新的概念&#xff0c;还没有着手去尝试&#xff0c;感觉很多内容确实有用&#xff0c;等我把这一章结束时候&#xff0c;实际跑一跑代码感受一下。 甩锅声明&#xff1a;本人英语一般&#xff0c;翻译只是为了做个笔记&#…

在Vue种使用Vant框架

第一步&#xff1a;打开Vant框架地址 https://vant-contrib.gitee.io/vant/v2/#/zh-CN/home 第二步&#xff1a; 安装 第三步&#xff1a;引入&#xff08;我这里使用的是按需导入&#xff09; 执行命令&#xff1a; npm i babel-plugin-import -D ①&#xff1a;src下创建个…

pycharm 配置github

文章目录 环境必备操作步骤1.在pycharm中配置git和github2.获取ssh密钥3.将本地项目与github仓库连接4.同步本地项目到github 相关问题参考文章 环境必备 pycharm 2020.1&#xff1a;集成开发环境&#xff0c;需要安装并配置环境 PyCharm 开发环境搭建指南&#xff1a;安装、配…

【RISC-V】寄存器及 PCS(过程调用标准)

文章目录 寄存器与别名函数入栈示例代码作用为什么需要保存 函数出栈示例代码作用为什么需要恢复 浮点寄存器的保存示例代码作用 浮点寄存器的恢复示例代码作用 寄存器与别名 Caller&#xff08;调用者&#xff09;指的是调用&#xff08;或执行&#xff09;一个函数的代码段或…

Pandas 和 CSV文件读取导出小纪

要从CSV文件中访问数据&#xff0c;我们需要一个函数read_csv()&#xff0c;它以数据帧的形式检索数据。 read_csv() 语法&#xff1a; pd.read_csv(filepath_or_buffer, sep’ ,’ , header’infer’, index_colNone, usecolsNone, engineNone, skiprowsNone, nrowsNone) 参…

vue启动编译时报错:134(内存溢出)

项目环境&#xff1a;win7 vue2 webpack2 最近开发过程中项目莫名其妙就起不来了&#xff0c;报错大致如下&#xff1a; 经过一番搜索&#xff0c;尝试了多种方法都不行,比如&#xff1a; 1. npm install increase-memory-limit npm install cross-env 在package.json中添加…

首个!AI开发者创作激励计划开启,有成长、有收入

各种视频网站都有什么创作激励&#xff01;那什么时候有专属于AI开发者的创作激励&#xff1f;好&#xff01;那AI开发者的福利来了&#xff01;&#xff01;既能潜心进行模型开发&#xff0c;又能提升技术能力&#xff0c;还能领一份创作金&#xff01;&#xff01;飞桨AI Stu…

Codeforces Round 882 (Div. 2)(视频讲解A——D)

[TOC](Codeforces Round 882 (Div. 2)&#xff08;视频讲解A——D&#xff09;) 讲解在B站&#xff1a;Codeforces Round 882 (Div. 2)&#xff08;视频讲解A——D&#xff09; A The Man who became a God #include<bits/stdc.h> #define endl \n #define INF 0x3f3…

k8s 大量 pod 处于 ContainerStatusUnknown 状态

如图所示&#xff0c;nexus 正常运行&#xff0c;但产生了大量的状态不明的 pod&#xff0c;原因也无从所知 解决办法&#xff0c;删除多余的 pod&#xff0c;一个一个删除&#xff0c;非常费劲 获取 namespace 中状态为 ContainerStatusUnknown 的 pod&#xff0c;并删除 …

从零学习微服务

更新中&#xff0c;关注不断更… 这里讲的是基于springboot和springboot alibaba的微服务&#xff0c;Spring Boot和Spring Boot Alibaba都是基于Spring框架的开源框架&#xff0c;用于简化应用程序的开发和部署。 这篇文章里会介绍微服务的整体概念&#xff0c;目前国内常用的…

CSDN周赛62期反馈及简要题解

持续了十期的《计算之魂》主题周赛告一段落&#xff0c;可能上周就已经告一段落了&#xff0c;以致于也出现了重复的考题。这本书确实不错&#xff0c;里面提到的计算机思维我认为是理解和学习计算机科学的基础。第一次读此书的时候就一口气读到第八章&#xff0c;读到精彩之处…

漏洞复现 || Hadoop未授权访问反弹Shell漏洞

免责声明 技术文章仅供参考,任何个人和组织使用网络应当遵守宪法法律,遵守公共秩序,尊重社会公德,不得危害网络安全,不得利用网络从事危害国家安全、荣誉和利益,未经授权请勿利用文章中的技术资料对任何计算机系统进行入侵操作。利用此文所提供的信息而造成的直接或间接…

ppt转pdf怎么转换?推荐这几种转换方法

ppt转pdf怎么转换&#xff1f;PPT转PDF可以确保演示文稿的格式、布局和字体在不同设备上的一致性。PDF文件在不同操作系统和软件中都能以相同的方式呈现&#xff0c;避免了因PPT文件打开时出现格式错误或乱码。可能很多人还不知道如何进行转换&#xff0c;下面就给大家推荐几种…

gitee 使用

1.打开git bash 2.cd 进入到合适位置 3.git clone 项目 4.配置用户名和email&#xff08;不然没法记录谁操作的&#xff09;

unordered系列的底层结构——哈希表

目录 哈希概念 哈希冲突 哈希函数 解决哈希冲突的方法 闭散列 线性探测 线性探测的实现 ​编辑 二次探测 开散列 开散列概念 开散列的实现 开散列增容 开散列的思考 哈希概念 顺序结构以及平衡树中&#xff0c;元素关键码与其存储位置之间没有对应的关系&#xf…

https证书已经部署到宝塔,但访问网站还显示不生效问题解决

先说解决方法&#xff1a;重启Nginx服务器即可&#xff0c;可以在宝塔面板右上角直接重启即可解决。 过程 腾讯云的https免费证书只有一年有效期&#xff0c;而且续期不能在原证书上续&#xff0c;只能替换。但是我替换后&#xff0c;访问网站异常。如下图&#xff1a; 可以看…

TypeScript 中的【声明合并】规则

概念&#xff1a; 在TS中&#xff0c;如果定义了多个相同命名的函数&#xff0c;接口或者class 类&#xff0c;那么它们会自动合并成一个类型 函数的合并&#xff1a; 前面章节讲解的函数重载就是使用了定义多个函数的类型进行合并&#xff1a; function reverse(x: number):…

UE4 关于使用Webbrowser插件遇到的问题以及解决办法

1.无法播放网页视频&#xff0c;这是因为UE4的WebBrowser自带的cef3为3071版本&#xff0c;默认不支持h264等直播流&#xff0c;导致web里的直播流无法播放 解决办法&#xff1a;第一种办法&#xff0c;重新编译了cef源码&#xff0c;改成支持H.264&#xff0c;然后在UE4安装目…