谈谈对vitejs预构建的理解(上)

简介: 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等),并不包括htmlvite是如何做到将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
          }
        }
      )



相关文章
|
关系型数据库 测试技术 Serverless
【PolarDB Serverless】资源伸缩&压测 TPC-C 测评
【PolarDB Serverless】资源伸缩&压测 TPC-C 测评
156318 31
【PolarDB Serverless】资源伸缩&压测 TPC-C 测评
|
8月前
|
开发框架 缓存 自然语言处理
HarmonyOS ArkTS声明式UI开发实战教程
本文深入探讨了ArkTS作为HarmonyOS生态中新一代声明式UI开发框架的优势与应用。首先对比了声明式与命令式开发的区别,展示了ArkTS如何通过直观高效的代码提升可维护性。接着分析了其核心三要素:数据驱动、组件化和状态管理,并通过具体案例解析布局体系、交互组件开发技巧及复杂状态管理方案。最后,通过构建完整TODO应用实战,结合调试优化指南,帮助开发者掌握声明式UI设计精髓,感受ArkTS的独特魅力。文章鼓励读者通过“破坏性实验”建立声明式编程思维,共同推动HarmonyOS生态发展。
459 3
|
资源调度 分布式计算 Kubernetes
Koordinator 支持 K8s 与 YARN 混部,小红书在离线混部实践分享
Koordinator 支持 K8s 与 YARN 混部,小红书在离线混部实践分享
|
机器学习/深度学习 编解码 人工智能
当前VR技术的限制与挑战:深入剖析与未来展望
【8月更文挑战第26天】当前VR技术在技术、内容、市场等多个层面仍面临诸多限制与挑战。然而,随着技术的不断创新和市场的逐步成熟,这些限制和挑战将逐渐得到克服。未来,VR技术有望在更多领域发挥重要作用,为用户带来更加丰富、便捷的沉浸式体验。我们期待VR技术的持续进步和广泛应用,共同见证这一科技领域的辉煌未来。
|
消息中间件 安全 Kafka
"深入实践Kafka多线程Consumer:案例分析、实现方式、优缺点及高效数据处理策略"
【8月更文挑战第10天】Apache Kafka是一款高性能的分布式流处理平台,以高吞吐量和可扩展性著称。为提升数据处理效率,常采用多线程消费Kafka数据。本文通过电商订单系统的案例,探讨了多线程Consumer的实现方法及其利弊,并提供示例代码。案例展示了如何通过并行处理加快订单数据的处理速度,确保数据正确性和顺序性的同时最大化资源利用。多线程Consumer有两种主要模式:每线程一个实例和单实例多worker线程。前者简单易行但资源消耗较大;后者虽能解耦消息获取与处理,却增加了系统复杂度。通过合理设计,多线程Consumer能够有效支持高并发数据处理需求。
456 4
|
11月前
|
SQL DataWorks 数据可视化
DataWorks产品体验与评测
在当今数字化时代,数据处理的重要性不言而喻。DataWorks作为一款数据开发治理平台,在数据处理领域占据着重要的地位。通过对DataWorks产品的体验使用,我们可以深入了解其功能、优势以及存在的问题,并且与其他数据处理工具进行对比,从而为企业、工作或学习中的数据处理提供有价值的参考。
456 6
DataWorks产品体验与评测
|
安全 云栖大会 云计算
首批Well-Architected生态合作伙伴揭晓,齐聚2024云栖大会
在帮助企业客户上好云、用好云、管好云的道路上,阿里云始终坚持开放合作的理念,不断寻求与优质的生态伙伴深化合作,携手共建Well-Architected技术与服务方案。在今年的云栖大会上,共七家合作伙伴企业荣获首批「Well-Architected阿里云卓越架构生态合作伙伴」认证。
588 2
|
算法 大数据 数据处理
【天衍系列 01】深入理解Flink的 FileSource 组件:实现大规模数据文件处理
【天衍系列 01】深入理解Flink的 FileSource 组件:实现大规模数据文件处理
457 4
|
应用服务中间件 网络安全 数据安全/隐私保护
使用 Nginx 实现 HTTPS 网站设置
HTTPS 其实是有两部分组成:HTTP + SSL/TLS,也就是在 HTTP 的基础上又加了一层处理加密信息的模块。服务端和客户端的信息传递都会通过 TLS 进行加密,所以传输的数据都是加密后的数据。
986 0
使用 Nginx 实现 HTTPS 网站设置
|
数据安全/隐私保护 iOS开发 开发者
苹果开发者账号注册及证书生成方法详解
苹果开发者账号注册及证书生成方法详解
413 1