vitejs预构建理解及流程解析

目录
  • 引言
  • 为啥要预构建
  • 预构建的流程
    • 1. 查找依赖
    • 2. 对查找到的依赖进行构建
  • 缓存与预构建
  • 总结

引言

vite在官网介绍中,第一条就提到的特性就是自己的本地冷启动极快。这主要是得益于它在本地服务启动的时候做了预构建。出于好奇,抽时间了解了下vite在预构建部分的主要实现思路,分享出来供大家参考。

为啥要预构建

简单来讲就是为了提高本地开发服务器的冷启动速度。按照vite的说法,当冷启动开发服务器时,基于打包器的方式启动必须优先抓取并构建你的整个应用,然后才能提供服务。随着应用规模的增大,打包速度显著下降,本地服务器的启动速度也跟着变慢。

为了加快本地开发服务器的启动速度,vite 引入了预构建机制。在预构建工具的选择上,vite选择了 esbuildesbuild 使用 Go 编写,比以 JavaScript 编写的打包器构建速度快 10-100 倍,有了预构建,再利用浏览器的esm方式按需加载业务代码,动态实时进行构建,结合缓存机制,大大提升了服务器的启动速度。

预构建的流程

1. 查找依赖

如果是首次启动本地服务,那么vite会自动抓取源代码,从代码中找到需要预构建的依赖,最终对外返回类似下面的一个deps对象:

{
  vue: '/path/to/your/project/node_modules/vue/dist/vue.runtime.esm-bundler.js',
  'element-plus': '/path/to/your/project/node_modules/element-plus/es/index.mjs',
  'vue-router': '/path/to/your/project/node_modules/vue-router/dist/vue-router.esm-bundler.js'
}

具体实现就是,调用esbuildbuild api,以index.html作为查找入口(entryPoints),将所有的来自node_modules以及在配置文件的optimizeDeps.include选项中指定的模块找出来。

//...省略其他代码
  if (explicitEntryPatterns) {
    entries = await globEntries(explicitEntryPatterns, config)
  } else if (buildInput) {
    const resolvePath = (p: string) => path.resolve(config.root, p)
    if (typeof buildInput === 'string') {
      entries = [resolvePath(buildInput)]
    } else if (Array.isArray(buildInput)) {
      entries = buildInput.map(resolvePath)
    } else if (isObject(buildInput)) {
      entries = Object.values(buildInput).map(resolvePath)
    } else {
      throw new Error('invalid rollupOptions.input value.')
    }
  } else {
    // 重点看这里:使用html文件作为查找入口
    entries = await globEntries('**/*.html', config)
  }
//...省略其他代码
build.onResolve(
        {
          // avoid matching windows volume
          filter: /^[\w@][^:]/
        },
        async ({ path: id, importer }) => {
          const resolved = await resolve(id, importer)
          if (resolved) {
            // 来自node_modules和在include中指定的模块
            if (resolved.includes('node_modules') || include?.includes(id)) {
              // dependency or forced included, externalize and stop crawling
              if (isOptimizable(resolved)) {
                // 重点看这里:将符合预构建条件的依赖记录下来,depImports就是对外导出的需要预构建的依赖对象
                depImports[id] = resolved
              }
              return externalUnlessEntry({ path: id })
            } else if (isScannable(resolved)) {
              const namespace = htmlTypesRE.test(resolved) ? 'html' : undefined
              // linked package, keep crawling
              return {
                path: path.resolve(resolved),
                namespace
              }
            } else {
              return externalUnlessEntry({ path: id })
            }
          } else {
            missing[id] = normalizePath(importer)
          }
        }
      )

但是熟悉esbuild的小伙伴可能知道,esbuild默认支持的入口文件类型有jstsjsxcssjsonbase64dataurlbinaryfile(.png等),并不包括html

vite是如何做到将index.html作为打包入口的呢?原因是vite自己实现了一个esbuild插件esbuildScanPlugin,来处理.vue.html这种类型的文件。

具体做法是读取html的内容,然后将里面的script提取到一个esm格式的js模块。

      // 对于html类型(.VUE/.HTML/.svelte等)的文件,提取文件里的script内容。html types: extract script contents -----------------------------------
      build.onResolve({ filter: htmlTypesRE }, async ({ path, importer }) => {
        const resolved = await resolve(path, importer)
        if (!resolved) return
        // It is possible for the scanner to scan html types in node_modules.
        // If we can optimize this html type, skip it so it's handled by the
        // bare import resolve, and recorded as optimization dep.
        if (resolved.includes('node_modules') && isOptimizable(resolved)) return
        return {
          path: resolved,
          namespace: 'html'
        }
      })
      // 配合build.onResolve,对于类html文件,提取其中的script,作为一个js模块extract scripts inside HTML-like files and treat it as a js module
      build.onLoad(
        { filter: htmlTypesRE, namespace: 'html' },
        async ({ path }) => {
          let raw = fs.readFileSync(path, 'utf-8')
          // Avoid matching the content of the comment
          raw = raw.replace(commentRE, '<!---->')
          const isHtml = path.endsWith('.html')
          const regex = isHtml ? scriptModuleRE : scriptRE
          regex.lastIndex = 0
          // js 的内容被处理成了一个虚拟模块
          let js = ''
          let scriptId = 0
          let match: RegExpExecArray | null
          while ((match = regex.exec(raw))) {
            const [, openTag, content] = match
            const typeMatch = openTag.match(typeRE)
            const type =
              typeMatch && (typeMatch[1] || typeMatch[2] || typeMatch[3])
            const langMatch = openTag.match(langRE)
            const lang =
              langMatch && (langMatch[1] || langMatch[2] || langMatch[3])
            // skip type="application/ld+json" and other non-JS types
            if (
              type &&
              !(
                type.includes('javascript') ||
                type.includes('ecmascript') ||
                type === 'module'
              )
            ) {
              continue
            }
            // 默认的js文件的loader是js,其他对于ts、tsx jsx有对应的同名loader
            let loader: Loader = 'js'
            if (lang === 'ts' || lang === 'tsx' || lang === 'jsx') {
              loader = lang
            }
            const srcMatch = openTag.match(srcRE)
            // 对于<script src='path/to/some.js'>引入的js,将它转换为import 'path/to/some.js'的代码
            if (srcMatch) {
              const src = srcMatch[1] || srcMatch[2] || srcMatch[3]
              js += `import ${JSON.stringify(src)}\n`
            } else if (content.trim()) {
              // The reason why virtual modules are needed:
              // 1. There can be module scripts (`<script context="module">` in Svelte and `<script>` in Vue)
              // or local scripts (`<script>` in Svelte and `<script setup>` in Vue)
              // 2. There can be multiple module scripts in html
              // We need to handle these separately in case variable names are reused between them
              // append imports in TS to prevent esbuild from removing them
              // since they may be used in the template
              const contents =
                content +
                (loader.startsWith('ts') ? extractImportPaths(content) : '')
                // 将提取出来的script脚本,存在以xx.vue?id=1为key的script对象中script={'xx.vue?id=1': 'js contents'}
              const key = `${path}?id=${scriptId++}`
              if (contents.includes('import.meta.glob')) {
                scripts[key] = {
                  // transformGlob already transforms to js
                  loader: 'js',
                  contents: await transformGlob(
                    contents,
                    path,
                    config.root,
                    loader,
                    resolve,
                    config.logger
                  )
                }
              } else {
                scripts[key] = {
                  loader,
                  contents
                }
              }
              const virtualModulePath = JSON.stringify(
                virtualModulePrefix + key
              )
              const contextMatch = openTag.match(contextRE)
              const context =
                contextMatch &&
                (contextMatch[1] || contextMatch[2] || contextMatch[3])
              // Especially for Svelte files, exports in <script context="module"> means module exports,
              // exports in <script> means component props. To avoid having two same export name from the
              // star exports, we need to ignore exports in <script>
              if (path.endsWith('.svelte') && context !== 'module') {
                js += `import ${virtualModulePath}\n`
              } else {
                // e.g. export * from 'virtual-module:xx.vue?id=1'
                js += `export * from ${virtualModulePath}\n`
              }
            }
          }
          // This will trigger incorrectly if `export default` is contained
          // anywhere in a string. Svelte and Astro files can't have
          // `export default` as code so we know if it's encountered it's a
          // false positive (e.g. contained in a string)
          if (!path.endsWith('.vue') || !js.includes('export default')) {
            js += '\nexport default {}'
          }
          return {
            loader: 'js',
            contents: js
          }
        }
      )

由上文我们可知,来自node_modules中的模块依赖是需要预构建的。

例如import ElementPlus from 'element-plus'。

因为在浏览器环境下,是不支持这种裸模块引用的(bare import)。

另一方面,如果不进行构建,浏览器面对由成百上千的子模块组成的依赖,依靠原生esm的加载机制,每个的依赖的import都将产生一次http请求。面对大量的请求,浏览器是吃不消的。

因此客观上需要对裸模块引入进行打包,并处理成浏览器环境下支持的相对路径或路径的导入方式。

例如:import ElementPlus from '/path/to/.vite/element-plus/es/index.mjs'。

2. 对查找到的依赖进行构建

在上一步,已经得到了需要预构建的依赖列表。现在需要把他们作为esbuildentryPoints打包就行了。

//使用esbuild打包,入口文件即为第一步中抓取到的需要预构建的依赖
    import { build } from 'esbuild'
   // ...省略其他代码
    const result = await build({
      absWorkingDir: process.cwd(),
     // flatIdDeps即为第一步中所得到的需要预构建的依赖对象
      entryPoints: Object.keys(flatIdDeps),
      bundle: true,
      format: 'esm',
      target: config.build.target || undefined,
      external: config.optimizeDeps?.exclude,
      logLevel: 'error',
      splitting: true,
      sourcemap: true,
// outdir指定打包产物输出目录,processingCacheDir这里并不是.vite,而是存放构建产物的临时目录
      outdir: processingCacheDir,
      ignoreAnnotations: true,
      metafile: true,
      define,
      plugins: [
        ...plugins,
        esbuildDepPlugin(flatIdDeps, flatIdToExports, config, ssr)
      ],
      ...esbuildOptions
    })
    // 写入_metadata文件,并替换缓存文件。Write metadata file, delete `deps` folder and rename the new `processing` folder to `deps` in sync
    commitProcessingDepsCacheSync()

vite并没有将esbuildoutdir(构建产物的输出目录)直接配置为.vite目录,而是先将构建产物存放到了一个临时目录。当构建完成后,才将原来旧的.vite(如果有的话)删除。然后再将临时目录重命名为.vite。这样做主要是为了避免在程序运行过程中发生了错误,导致缓存不可用。

  function commitProcessingDepsCacheSync() {
    // Rewire the file paths from the temporal processing dir to the final deps cache dir
    const dataPath = path.join(processingCacheDir, '_metadata.json')
    writeFile(dataPath, stringifyOptimizedDepsMetadata(metadata))
    // Processing is done, we can now replace the depsCacheDir with processingCacheDir
    // 依赖处理完成后,使用依赖缓存目录替换处理中的依赖缓存目录
    if (fs.existsSync(depsCacheDir)) {
      const rmSync = fs.rmSync ?? fs.rmdirSync // TODO: Remove after support for Node 12 is dropped
      rmSync(depsCacheDir, { recursive: true })
    }
    fs.renameSync(processingCacheDir, depsCacheDir)
  }
}

以上就是预构建的主要处理流程。

缓存与预构建

vite冷启动之所以快,除了esbuild本身构建速度够快外,也与vite做了必要的缓存机制密不可分。

vite在预构建时,除了生成预构建的js文件外,还会创建一个_metadata.json文件,其结构大致如下:

{
  "hash": "22135fca",
  "browserHash": "632454bc",
  "optimized": {
    "vue": {
      "file": "/path/to/your/project/node_modules/.vite/vue.js",
      "src": "/path/to/your/project/node_modules/vue/dist/vue.runtime.esm-bundler.js",
      "needsInterop": false
    },
    "element-plus": {
      "file": "/path/to/your/project/node_modules/.vite/element-plus.js",
      "src": "/path/to/your/project/node_modules/element-plus/es/index.mjs",
      "needsInterop": false
    },
    "vue-router": {
      "file": "/path/to/your/project/node_modules/.vite/vue-router.js",
      "src": "/path/to/your/project/node_modules/vue-router/dist/vue-router.esm-bundler.js",
      "needsInterop": false
    }
  }
}

hash 是缓存的主要标识,由vite的配置文件和项目依赖决定(依赖的信息取自package-lock.jsonyarn.lockpnpm-lock.yaml)。 所以如果用户修改了vite.config.js或依赖发生了变化(依赖的添加删除更新会导致lock文件变化)都会令hash发生变化,缓存也就失效了。这时,vite需要重新进行预构建。当然如果手动删除了.vite缓存目录,也会重新构建。

// 基于配置文件+依赖信息生成hash
const lockfileFormats = ['package-lock.json', 'yarn.lock', 'pnpm-lock.yaml']
function getDepHash(root: string, config: ResolvedConfig): string {
  let content = lookupFile(root, lockfileFormats) || ''
  // also take config into account
  // only a subset of config options that can affect dep optimization
  content += JSON.stringify(
    {
      mode: config.mode,
      root: config.root,
      define: config.define,
      resolve: config.resolve,
      buildTarget: config.build.target,
      assetsInclude: config.assetsInclude,
      plugins: config.plugins.map((p) => p.name),
      optimizeDeps: {
        include: config.optimizeDeps?.include,
        exclude: config.optimizeDeps?.exclude,
        esbuildOptions: {
          ...config.optimizeDeps?.esbuildOptions,
          plugins: config.optimizeDeps?.esbuildOptions?.plugins?.map(
            (p) => p.name
          )
        }
      }
    },
    (_, value) => {
      if (typeof value === 'function' || value instanceof RegExp) {
        return value.toString()
      }
      return value
    }
  )
  return createHash('sha256').update(content).digest('hex').substring(0, 8)
}

vite启动时首先检查hash的值,如果当前的hash值与_metadata.json中的hash值相同,说明项目的依赖没有变化,无需重复构建了,直接使用缓存即可。

// 计算当前的hash
const mainHash = getDepHash(root, config)
 const metadata: DepOptimizationMetadata = {
    hash: mainHash,
    browserHash: mainHash,
    optimized: {},
    discovered: {},
    processing: processing.promise
  }
 let prevData: DepOptimizationMetadata | undefined
    try {
      const prevDataPath = path.join(depsCacheDir, '_metadata.json')
      prevData = parseOptimizedDepsMetadata(
        fs.readFileSync(prevDataPath, 'utf-8'),
        depsCacheDir,
        processing.promise
      )
    } catch (e) { }
    // hash is consistent, no need to re-bundle
    // 比较缓存的hash与当前hash
    if (prevData && prevData.hash === metadata.hash) {
      log('Hash is consistent. Skipping. Use --force to override.')
      return {
        metadata: prevData,
        run: () => (processing.resolve(), processing.promise)
      }
    }

总结

以上就是vite预构建的主要处理逻辑,总结起来就是先查找需要预构建的依赖,然后将这些依赖作为entryPoints进行构建,构建完成后更新缓存。vite在启动时为提升速度,会检查缓存是否有效,有效的话就可以跳过预构建环节,缓存是否有效的判定是对比缓存中的hash值与当前的hash值是否相同。由于hash的生成算法是基于vite配置文件和项目依赖的,所以配置文件和依赖的的变化都会导致hash发生变化,从而重新进行预构建。

更多关于vitejs预构建流程的资料请关注我们其它相关文章!,希望大家以后多多支持我们!

(0)

相关推荐

  • 如何用Vite构建工具快速创建Vue项目

    目录 和Webpack相比,Vite具有以下特点 Vite构建Vue项目 构建过程可能会发生的一些问题 总结 和Webpack相比,Vite具有以下特点 1.快速的冷启动,不需要等待打包 2.即时的热模块更新,真正的按需编译,不用等待整个项目编译完成 Vite构建Vue项目 前提:安装Node.js和Vite 第一步通过npm创建Vite项目 npm init vite-app 项目名称 # 例如 npm init vite-app HelloVue 第二步当项目创建成功后,cd到项目目录 cd

  • electron-vite新一代electron开发构建工具

    目录 前言 electron-vite 是什么 特性 安装 开发&编译 推荐项目目录 开始学习 配置 配置文件 配置智能提示 预设配置 基于主进程的编译项预设: 基于preload脚本的编译项预设: 基于渲染进程的编译项预设: 配置问题 如果 Electron 具有多窗口应该如何配置? 结语 前言 得益于 Vite 卓越的前端开发体验,越来越多的 Electron 项目也开始应用它来构建开发.翻阅各种社区资源可以发现很多基于 Vite 搭建的 Electron 开发模板,但都存在着一些共同的问题

  • 使用Vite+Vue3+Vant全家桶快速构建项目步骤详解

    目录 引言 一.vue3全家桶模板介绍 1.版本依赖 2.全家桶内置集成 二.安装 tive-cli 命令行工具 三.生成项目 四.项目体验 引言 随着Vue3 和 Vite 版本的不断更新完善,开发体验有了质的飞跃.因此,越来越多的大厂也逐步拥抱 Vue3. 利用Vite 脚手架工具可以很轻松生成以 Vue3 为模板的项目,但是作为Vue全家桶的 vue-router.vuex.axios等成员,需要自己一个一个去配置.于是便自行开发了本文讲到的 tive-cli 脚手架模板工具,只需短短几个

  • vite构建项目并支持微前端

    目录 基础配置 支持微前端构建 其他说明 1. 老旧浏览器的支持 2. 关于 TypeScript 的说明 3. 对接 CDN 4. 构建出错 4.1 找不到包 4.2 请求超时 4.3 导入模块出错 小结 得益于 esbuild 的超高性能,vite 在诞生之初就备受关注,且一直保持着活跃的开发迭代.截至目前,vite 已经迭代到了 2.7.10 版本,各方面也基本具备了在生产使用的条件.这段时间,我在项目中尝试了使用 vite 进行打包构建,本文就是这次构建的过程记录. 基础配置 首先使用v

  • Vite+Electron快速构建VUE3桌面应用的实现

    目录 一. 简介 二. 创建一个Vite项目 1. 安装 vite 2. 创建项目 3. 进入且运行 三. 配置Electron 1. 官方文档 2. 安装 3. 配置文件 四. 运行 五. 最后 一. 简介 首先,介绍下vite和Electron. Vite是一种新型前端构建工具,能够显著提升前端开发体验.由尤大推出,其发动态表示"再也回不去webpack了..." Electron是一个使用 JavaScript.HTML 和 CSS 构建桌面应用程序的框架. 嵌入Chromium

  • vitejs预构建理解及流程解析

    目录 引言 为啥要预构建 预构建的流程 1. 查找依赖 2. 对查找到的依赖进行构建 缓存与预构建 总结 引言 vite在官网介绍中,第一条就提到的特性就是自己的本地冷启动极快.这主要是得益于它在本地服务启动的时候做了预构建.出于好奇,抽时间了解了下vite在预构建部分的主要实现思路,分享出来供大家参考. 为啥要预构建 简单来讲就是为了提高本地开发服务器的冷启动速度.按照vite的说法,当冷启动开发服务器时,基于打包器的方式启动必须优先抓取并构建你的整个应用,然后才能提供服务.随着应用规模的增大

  • React实时预览react-live源码解析

    目录 引言 源码解读 输入内容 Provider generateElement 其他组件 总结 引言 react-live 是一个 react 的实时编辑器,可直接编辑 react 代码,并实时预览.可以看下官方的预览图: 本文针对的源码版本 src ├── components │ ├── Editor │ │ └── index.js │ └── Live │ ├── LiveContext.js │ ├── LiveEditor.js │ ├── LiveError.js │ ├── L

  • Kubernetes Informer数据存储Index与Pod分配流程解析

    目录 确立目标 Process 查看消费的过程 Index 掌握Index数据结构 distribute 信息的分发distribute 理解一个pod的被调度的大致流程 Scheduler SchedulingQueue scheduleOne ScheduleResult 调度计算结果 Assume 初步推算 Bind 实际绑定 Update To Etcd Summary 确立目标 理解Informer的数据存储方式 大致理解Pod的分配流程 理解Informer的数据存储方式 代码在k8

  • 使用Jersey构建图片服务器过程解析

    这篇文章主要介绍了使用Jersey构建图片服务器过程解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 前台页面代码 <form id="jvForm" action="add.do" method="post" enctype="multipart/form-data"> <table> <tr> <td width="2

  • python manage.py runserver流程解析

    这篇文章主要介绍了python manage.py runserver流程解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 版本 python27 django 1.0 搭建可运行的环境 创建python27 虚拟环境 github 下载 django-1.0.tar.gz(1.0 版本的django) 解压 可以看到,有个 demo 在 examples 目录 把 django 目录拷贝到 examples 下面,这样 example 可以

  • Android9.0 SystemUI 网络信号栏定制修改的流程解析

    前情提要 Android 8.1平台SystemUI 导航栏加载流程解析 9.0 改动点简要说明 1.新增 StatusBarMobileView 替代 SignalClusterView,用以控制信号栏显示 同时增加的还有 StatusBarIconView.StatusBarWifiView 2.整体流程和 8.1 类似 效果图 整体流程图 上代码 先来看初始赋值的地方 MobileSignalController.java,在 notifyListeners() 方法中进行我们对应的定制,

  • activiti实现员工请假流程解析

    源码下载: http://xiazai.jb51.net/202007/yuanma/FirstActiviti_jb51.rar 链接: https://pan.baidu.com/s/1tCN7SDAdEUerZxcTr_9cqA 提取码: twmp 在开始之前,先说一下刚开始学习工作流的时候遇到的问题,感觉比较困惑,经过这两天的学习,也算有所收获 1.部署了多个流程变量,如何准确开启特定的流程 2. 在一个流程的执行过程中,怎么确定执行到哪一步,即执行到哪个任务了 3. 在有多种情况的条件

  • 采用React编写小程序的Remax框架的编译流程解析(推荐)

    Remax是蚂蚁开源的一个用React来开发小程序的框架,采用运行时无语法限制的方案.整体研究下来主要分为三大部分:运行时原理.模板渲染原理.编译流程:看了下现有大部分文章主要集中在Reamx的运行时和模板渲染原理上,而对整个React代码编译为小程序的流程介绍目前还没有看到,本文即是来补充这个空白. 关于模板渲染原理看这篇文章:https://www.jb51.net/article/132635.htm 关于remax运行时原理看这篇文章:https://www.jb51.net/artic

  • Openstack各组件逻辑关系及运行流程解析

    目录 各组件逻辑关系图 Openstack 新建云主机流程图 虚拟机启动过程 各组件逻辑关系图 Keystone:认证服务 Glance:镜像服务 Nova:计算服务 Neutron:网络服务 Cinder:存储服务 Horizon:web 界面 Cellometer:监控计费 Swit:对象存储 Heat:编排服务(通过剧本,批量部署虚拟机) Openstack 新建云主机流程图 keystone 身份认证 填写创建云主机的相关配置–> nova-api --> 将相关信息保存到 MySQL

  • Python可视化程序调用流程解析

    目录 引言 安装 graphviz 工具 实战 引言 今天我们来分享一个 Python 领域的神级第三方库 -- pycallgraph,通过该库并结合 graphviz 工具,就可以非常方便的完成 Python 应用程序调用流程的可视化工作 我们先来看下效果图 怎么样,很是惊艳吧~ 下面我们就来一起完成这个可视化过程 安装 graphviz 工具 生成图片的过程,是依赖工具 graphviz 的,我们先进行下载安装 下载地址 www.graphviz.org/download/ 详细对于 gr

随机推荐