最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • Vite 依赖预编译,缩短数倍的冷启动时间

    正文概述 掘金(五柳)   2021-02-18   803

    前言

    前段时间,Vite 做了一个优化依赖预编译(Dependency Pre-Bundling)。简而言之,它指的是 Vite 会在 DevServer 启动前对需要预编译的依赖进行编译,然后在分析模块的导入(import)时会动态地应用编译过的依赖

    这么一说,我想大家可能立马会抛出一个疑问:Vite 不是 No Bundle 吗?确实 Vite 是 No Bundle,但是依赖预编译并不是意味着 Vite 要走向 Bundle,我们不要急着下定义,因为它的存在必然是有着其实际的价值

    那么,今天本文将会围绕以下 3 点来和大家一起从疑问点出发,深入浅出一番 Vite 的依赖预编译过程:

    • 什么是依赖预编译

    • 依赖预编译的作用

    • 依赖预编译的实现(源码分析)

    一、什么是依赖预编译

    当你在项目中引用了 vuelodash-es,那么你在启动 Vite 的时候,你会在终端看到这样的输出内容:

    Vite 依赖预编译,缩短数倍的冷启动时间

    而这表示 Vite 将你在项目中引入的 vuelodash-es 进行了依赖预编译!这里,我们通过大白话认识一下 Vite 的依赖预编译:

    • 默认情况下,Vite 会将 package.json 中生产依赖 dependencies 的部分启用依赖预编译,即会先对该依赖进行编译,然后将编译后的文件缓存在内存中(node_modules/.vite 文件下),在启动 DevServer 时直接请求该缓存内容。

    • 在 vite.config.js 文件中配置 optimizeDeps 选项可以选择需要或不需要进行预编译的依赖的名称,Vite 则会根据该选项来确定是否对该依赖进行预编译。

    • 在启动时添加 --force options,可以用来强制重新进行依赖预编译。

    Vite 依赖预编译,缩短数倍的冷启动时间

    所以,回到文章开始所说的疑问,这里我们可以这样理解依赖预编译,它的出现是一种优化,即没有它其实 No Bundle 也可以,有它更好(xiang)! 而且,依赖预编译并非无米之炊,Vite 也是受 Snowpack 的启发才提出的。

    那么,下面我们就来了解一下依赖预编译的作用是什么,即优化的意义~

    二、依赖预编译的作用

    对于依赖预编译的作用,Vite 官方也做了详细的介绍。那么,这里我们通过结合图例的方式来认识一下,具体会是两点:

    1. 兼容 CommonJS 和 AMD 模块的依赖

    因为 Vite 的 DevServer 是基于浏览器的 Natvie ES Module 实现的,所以对于使用的依赖如果是 CommonJS 或 AMD 的模块,则需要进行模块类型的转化(ES Module)。

    2. 减少模块间依赖引用导致过多的请求次数

    通常我们引入的一些依赖,它自己又会一些其他依赖。官方文档中举了一个很经典的例子,当我们在项目中使用 lodash-es 的时候:

    import { debounce } from "lodash-es"
    

    如果在没用依赖预编译的情况下,我们打开页面的 Dev Tool 的 Network 面板:

    Vite 依赖预编译,缩短数倍的冷启动时间

    可以看到此时大概有 600+ 和 lodash-es 相关的请求,并且所有请求加载花了 1.11 s,似乎还好?现在,我们来看一下使用依赖预编译的情况:

    Vite 依赖预编译,缩短数倍的冷启动时间

    此时,只有 1 个和 lodash-es 相关的请求(经过预编译),并且所有请求加载才花了 142 ms,缩短了足足 7 倍多的时间! 而这里节省的时间,就是我们常说的冷启动时间。

    那么,到这里我们就已经了解了 Vite 依赖预编译概念和作用。我想大家都会好奇这个过程又是怎么实现的?下面,我们就深入 Vite 源码来更进一步地认识依赖预编译过程!

    三、依赖预编译的实现

    在 Vite 源码中,默认的依赖预编译过程会在 DevServer 开启之前进行。这里,我们仍然以在项目中引入了 vuelodash-es 依赖为例。

    3.1 Dev Server 启动前

    首先,Vite 会创建一个 DevServer,也就是我们平常使用的本地开发服务器,这个过程是由 createServer 函数完成:

    // packages/vite/src/node/server/index.ts
    async function createServer(
      inlineConfig: InlineConfig = {}
    ): Promise<ViteDevServer> {
      ...
      // 通常情况下我们会命中这个逻辑
      if (!middlewareMode && httpServer) {
        // 重写 DevServer 的 listen,保证在 DevServer 启动前进行依赖预编译
        const listen = httpServer.listen.bind(httpServer)
        httpServer.listen = (async (port: number, ...args: any[]) => {
          try {
            ...
            // 依赖预编译相关
            await runOptimize()
          } 
          ...
        }) as any
        ...
      } else {
        await runOptimize()
      }
      ...
    }
    

    可以看到在 DevServer 真正启动之前,它会先调用 runOptimize 函数,进行依赖预编译相关的处理(用 bind 进行简单的重写)。

    runOptimize 函数:

    // packages/vite/src/node/server/index.ts
    const runOptimize = async () => {
      // config.optimzizeCacheDir 指的是 node_modules/.vite
      if (config.optimizeCacheDir) {
        ..
        try {
          server._optimizeDepsMetadata = await optimizeDeps(config)
        }
        ..
        server._registerMissingImport = createMissingImpoterRegisterFn(server)
      }
    }
    

    runOptimize 函数负责的是调用和注册处理依赖预编译相关的 optimizeDeps 函数,具体来说会是两件事:

    1. 进行依赖预编译

    optimizeDeps 函数是 Vite 实现依赖预编译的核心函数,它会根据配置 vite.config.js 的 optimizeDeps 选项和 package.json 的 dependencies 的参数进行第一次预编译。它会返回解析 node_moduels/.vite/_metadata.json 文件后生成的对象(包含预编译后的依赖所在的文件位置、原文件所处的文件位置等)。

    _metadata.json 文件:

    {
      "hash": "bade5e5e",
      "browserHash": "830194d7",
      "optimized": {
        "vue": {
          "file": "/Users/wjc/Documents/FE/demos/vite2.0-demo/node_modules/.vite/vue.js",
          "src": "/Users/wjc/Documents/FE/demos/vite2.0-demo/node_modules/vue/dist/vue.runtime.esm-bundler.js",
          "needsInterop": false
        },
        "lodash-es": {
          "file": "/Users/wjc/Documents/FE/demos/vite2.0-demo/node_modules/.vite/lodash-es.js",
          "src": "/Users/wjc/Documents/FE/demos/vite2.0-demo/node_modules/lodash-es/lodash.js",
          "needsInterop": false
        }
      }
    }
    

    这里,我们来分别认识一下这 4 个属性的含义:

    • hash 由需要进行预编译的文件内容生成的,用于防止 DevServer 启动时重复编译相同的依赖,即依赖并没有发生变化,不需要重新编译。

    • browserHashhash 和在运行时发现的额外的依赖生成的,用于让预编译的依赖的浏览器请求无效。

    • optimized 包含每个进行过预编译的依赖,其对应的属性会描述依赖源文件路径 src 和编译后所在路径 file

    • needsInterop 主要用于在 Vite 进行依赖性导入分析,这是由 importAnalysisPlugin 插件中的 transformCjsImport 函数负责的,它会对需要预编译且为 CommonJS 的依赖导入代码进行重写。举个例子,当我们在 Vite 项目中使用 react 时:

    import React, { useState, createContext } from 'react'
    

    此时 react 它是属于 needsInteroptrue 的范畴,所以 importAnalysisPlugin 插件的会对导入 react 的代码进行重写:

    import $viteCjsImport1_react from "/@modules/react.js";
    const React = $viteCjsImport1_react;
    const useState = $viteCjsImport1_react["useState"];
    const createContext = $viteCjsImport1_react["createContext"];
    

    之所以要进行重写的缘由是因为 CommonJS 的模块并不支持命名方式的导出。所以,如果不经过插件的转化,则会看到这样的异常:

    Uncaught SyntaxError: The requested module '/@modules/react.js' does not provide an export named 'useState'
    

    2. 注册依赖预编译相关函数

    调用 createMissingImpoterRegisterFn 函数,它会返回一个函数,其仍然内部会调用 optimizeDeps 函数进行预编译,只是不同于第一次预编译过程,此时会传人一个 newDeps,即新的需要进行预编译的依赖。

    那么,显然无论是第一次预编译,还是后续的预编译,它们两者的实现都是调用的 optimizeDeps 函数。所以,下面我们来看一下 optimizeDeps 函数~

    3.2 预编译实现核心 optimizeDeps 函数

    optimizeDeps 函数被定义在 packages/vite/node/optimizer/index.ts 中,它负责对依赖进行预编译过程:

    // packages/vite/node/optimizer/index.ts
    export async function optimizeDeps(
      config: ResolvedConfig,
      force = config.server.force,
      asCommand = false,
      newDeps?: Record<string, string>
    ): Promise<DepOptimizationMetadata | null> {
    ...
    }
    

    由于 optimizeDeps 内部逻辑较为繁多,这里我们拆分为 5 个步骤讲解:

    1. 读取该依赖此时的文件信息

    既然是编译依赖,很显然的是每次编译都需要知道此时文件内容对应的 Hash 值,以便于依赖发生变化时可以重新进行依赖编译,从而应用最新的依赖内容。

    所以,这里会先调用 getDepHash 函数获取依赖的 Hash 值:

    // 获取该文件此时的 hash
    const mainHash = getDepHash(root, config)
    const data: DepOptimizationMetadata = {
      hash: mainHash,
      browserHash: mainHash,
      optimized: {}
    }
    

    2. 对比缓存文件的 Hash

    前面,我们也提及了如果启动 Vite 时使用了 --force Option,则会强制重新进行依赖预编译。所以,当不是 --force 场景时,则会进行比较新旧依赖的 Hash 值的过程:

    // 默认为 false
    if (!force) {
      let prevData
      try {
        // 获取到此时缓存(本地磁盘)中编译的文件信息
        prevData = JSON.parse(fs.readFileSync(dataPath, 'utf-8'))
      } catch (e) {}
      // 对比此时的 
      if (prevData && prevData.hash === data.hash) {
        log('Hash is consistent. Skipping. Use --force to override.')
        return prevData
      }
    }
    

    可以看到如果新旧依赖的 Hash 值相等的时候,则会直接返回旧的依赖内容。

    3. 缓存失效或未缓存

    如果上面的 Hash 不等,则表示缓存失效,所以会删除 cacheDir 文件夹,又或者此时未进行缓存,即第一次依赖预编译逻辑( cacheDir 文件夹不存在),则创建 cacheDir 文件夹:

    if (fs.existsSync(cacheDir)) {
        emptyDir(cacheDir)
      } else {
        fs.mkdirSync(cacheDir, { recursive: true })
      }
    

    前面在讲 DevServer 启动时,我们提及预编译过程会分为两种:第一次预编译和后续的预编译。两者的区别在于后者会传入一个 newDeps,它表示新的需要进行预编译的依赖:

    let deps: Record<string, string>, missing: Record<string, string>
    if (!newDeps) {
      ;({ deps, missing } = await scanImports(config))
    } else {
      // 存在 newDeps 的时候,直接将 newDeps 赋值给 deps
      deps = newDeps
      missing = {}
    }
    

    并且,这里可以看到对于前者,第一次预编译,则会调用 scanImports 函数来找出和预编译相关的依赖 depsdeps 会是一个对象:

    {
      lodash-es:'/Users/wjc/Documents/FE/demos/vite2.0-demo/node_modules/lodash-es/lodash.js'
      vue:'/Users/wjc/Documents/FE/demos/vite2.0-demo/node_modules/vue/dist/vue.runtime.esm-bundler.js'
    }
    

    missing 则表示在 node_modules 中没找到的依赖。所以,当 missing 存在时,你会看到这样的提示:

    Vite 依赖预编译,缩短数倍的冷启动时间

    那么,回到上面对于后者(newDeps 存在时)的逻辑则较为简单,会直接给 deps 赋值为 newDeps,并且不需要处理 missing。因为,newDeps 只有在后续导入并安装了新的 dependencies 依赖,才会传入的,此时是不存在 missing 的依赖的( Vite 内置的 importAnalysisPlugin 插件会提前过滤掉这些)。

    4. 处理 optimizeDeps.include 相关依赖

    在前面,我们也提及了需要进行编译的依赖也会由 vite.config.js 的 optimizeDeps 选项决定。所以,在处理完 dependencies 之后,接着需要处理 optimizeDeps

    此时,会遍历前面从 dependencies 获取到的 deps,判断 optimizeDeps.iclude(数组)所指定的依赖是否存在,不存在则会抛出异常:

    const include = config.optimizeDeps?.include
      if (include) {
        const resolve = config.createResolver({ asSrc: false })
        for (const id of include) {
          if (!deps[id]) {
            const entry = await resolve(id)
            if (entry) {
              deps[id] = entry
            } else {
              throw new Error(
                `Failed to resolve force included dependency: ${chalk.cyan(id)}`
              )
            }
          }
        }
      }
    

    5. 使用 esbuild 编译依赖

    那么,在做好上述和预编译依赖相关的处理(文件 hash 生成、预编译依赖确定等)后。则进入依赖预编译的最后一步,使用 esbuild 来对相应的依赖进行编译:

      ...
      const esbuildService = await ensureService()
      await esbuildService.build({
        entryPoints: Object.keys(flatIdDeps),
        bundle: true,
        format: 'esm',
        ...
      })
      ...
    

    ensureService 函数是 Vite 内部封装的 util,它的本质是创建一个 esbuildservice,使用 service.build 函数来完成编译过程。

    此时,传入的 flatIdDeps 参数是一个对象,它是由上面提及的 deps 收集好的依赖创建的,它的作用是为 esbuild 进行编译的时候提供多路口(entry),flatIdDeps 对象:

    {
      lodash-es:'/Users/wjc/Documents/FE/demos/vite2.0-demo/node_modules/lodash-es/lodash.js'
      moment:'/Users/wjc/Documents/FE/demos/vite2.0-demo/node_modules/moment/dist/moment.js'
      vue:'/Users/wjc/Documents/FE/demos/vite2.0-demo/node_modules/vue/dist/vue.runtime.esm-bundler.js'
    }
    

    好了,到此我们已经分析完了整个依赖预编译的实现 ?(手动给看到这的大家?)。

    那么,接下来在 DevServer 启动后,当模块需要请求经过预编译的依赖的时候,Vite 内部的 resolvePlugin 插件会解析该依赖是否存在 seen 中(seen 中会存储编译过的依赖映射),是则直接应用 node_modules/.vite 目录下对应的编译后的依赖,避免直接去请求编译前的依赖的情况出现,从而缩短冷启动的时间。

    结语

    通过了解 Vite 依赖预编译的作用、实现等相关知识,我想大家应该不会再去纠结 Bundle 或者 No Bundle 的问题了,仍然是那句话,存在即有价值。并且,依赖预编译这个知识点在面试场景下,可能也是一个很有趣的考题 ?。最后,如果文章中存在表达不当或错误的地方,欢迎大家提 Issue~

    点赞 ?

    通过阅读本篇文章,如果有收获的话,可以点个赞,这将会成为我持续分享的动力,感谢~


    起源地下载网 » Vite 依赖预编译,缩短数倍的冷启动时间

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元