《从 Webpack 到 Vite 系列二 》Vite

·  阅读 795
《从 Webpack 到 Vite 系列二 》Vite

引用

Vite介绍

背景

近些年前端工程化发展迅速,各种构建工具层出不穷。下面是按照 npm 发版时间线列出的开发者比较熟知的一些构建工具。

image.png 但是目前Webpack仍然占据统治地位,npm 每周下载量达到两千多万次。

《从 Webpack 到 Vite 系列一》大话 Webpack 一文中,我们学习了解了webpack通过打包流程 抓取--编译--构建,可以生成一份编译、优化后能良好兼容各个浏览器的生产环境代码。在开发环境中流程也基本相同,先把整个应用构建打包后,再把打包后的代码交给 dev-server

但随着前端业务的复杂化,js代码量呈指数增长,打包构建时间越来越久,dev server(开发服务器)性能遇到瓶颈:

· 缓慢的服务启动: 大型项目中dev server启动时间达到几十秒甚至几分钟。
· 缓慢的HMR热更新: 即使采用了 HMR 模式,其热更新速度也会随着应用规模的增长而显著下降,已达到性能瓶颈,无多少优化空间。

面对永远学不完的前端技术,卷不动的前端开发再加上缓慢的开发环境,大大降低了开发者的幸福感。 vue 的作者尤雨溪在开发 vue3.0 的时候顺便开发的一个 基于原生 ES-Module 的前端构建工具 —— Vite 应运而生

什么是 Vite ?

基于esbuild与Rollup,依靠浏览器自身ESM编译功能, 实现极致开发体验的新一代构建工具!

Vite(法语意为 "快速的",发音同 "veet")是一种新型前端构建工具,能够显著提升前端开发体验。它主要由两部分组成:

  • 开发服务器,它==基于原生 ES 模块==提供了丰富的内建功能,利用浏览器的ESM能力来提供源文件,具有丰富的内置功能并具有高效的HMR。
  • ==一套构建指令==,它使用 Rollup 打包你的代码,并且它是预配置的,可输出用于生产环境的高度优化过的静态资源。

不管是官网还是各种关于Vite的文章,甚至从尤雨溪口中,我们对 Vite 最初了解就是 快!快!非常快!!

Vite作为一个基于浏览器原生ESM的构建工具,它省略了开发环境的打包过程,利用浏览器去解析imports,在服务端按需编译返回。同时,在开发环境拥有速度快到惊人的模块热更新,且热更新的速度不会随着模块增多而变慢。因此,使用Vite进行开发,至少会比Webpack快10倍左右。

其实 Vite 不止于此,它的四大特点如下

  • 快速的冷启动
  • 即时的模块热更新
  • 真正的按需编译
  • 开箱即用

在深入介绍 Vite 前,我们再复习两个概念。

ES Modules、EsBuild

ES Modules

问:ES Modules 是什么 ?

答:ES Modules 是用于处理模块的 ECMAScript 标准。这个标准化过程在 ES6 中完成,浏览器开始实施这个标准,试图以相同的工作方式保持一致性,现在 Chrome,Safari,Edge 和 Firefox(从 60 版本开始)支持 ES 模块。

问:ES Modules 解决了什么问题 ?

答:将你需要的函数和变量变成一个模块,以导入导出的方式把你的代码像组合乐高积木一样,来使用同样的模块创建不同的应用。

问:ES Modules 解析过程 ?

答:

  import HelloWorld from './components/HelloWorld.vue'
  export default {
    name: 'App',
    components: {
      HelloWorld
    }
  }
复制代码

当浏览器解析 import HelloWorld from './components/HelloWorld.vue' 时, 会向当前域名发送一个请求获取对应的资源(ESM支持解析相对路径)。

image.png

浏览器下载对应的文件,然后解析成模块记录。接下来会进行实例化,为模块分配内存,然后按照导入、导出语句建立模块和内存的映射关系。 最后,运行上述代码,把内存空间填充为真实的值。

EsBuild 又是什么

问:EsBuild 又是什么 ?

答:ESbuild 是一个类似webpack构建工具。它的构建速度是 webpack 的几十倍。

问:EsBuild 为什么这么快 ?

答:

image.png

  • JS 是单线程串行,EsBuild 是新开一个进程,然后多线程并行,充分发挥多核优势
  • Go 是纯机器码,肯定要比 JIT 快
  • 不使用 AST,优化了构建流程

推荐阅读:ESbuild 介绍

介绍完背景和几个概念,我们接下来通过分析和介绍他的功能去深入了解一下 Vite,Lets Go!

Vite 丰富的内建功能

实现核心

启动一个 koa 服务器拦截由浏览器请求 ESM的请求。通过请求的路径找到目录下对应的文件做一定的处理最终以 ESM的格式返回给客户端。

image.png

依赖处理

Vite 通过在一开始将应用中的模块区分为 依赖源码 两类,改进了开发服务器启动时间。依赖 大多为在开发时不会变动的纯 JavaScript。一些较大的依赖(例如有上百个模块的组件库)处理的代价也很高

image.png

可以观察到浏览器请求 vue.js 时,被认为是一个 node_modules 模块。

平时开发中,webpack & rollup(rollup有对应插件) 等打包工具会帮我们找到模块的路径。

但浏览器只能通过相对路径去寻找,若直接使用模块名比如:import vue from 'vue',浏览器就会报错,这个时候就需要一个三方包进行处理。

Vite 对 ESM 形式的 js 文件模块使用了 ES Module Lexer 处理。Lexer 会找到代码中以 import 语法导入的模块并以数组形式返回。Vite 通过该数组的值获取判断是否为一个 node_modules 模块。若是则进行对应改写成 @modules/:id 的写法。

重写完路径后,浏览器会发送 path 为 /@modules/:id 的对应请求,接下来会被 Vite 客户端做一层拦截来解析模块的真实位置。

推荐阅读:Es-Module-Lexer,ES Module 语法的词法分析利器

依赖预构建

当我们首次启动 Vite ,会注意到打印出了以下信息

image.png

这个过程就是 Vite 的预构建,接下来我们对整个流程进行分析和学习。

什么是预构建

顾名思义:提前构建一下

为什么要进行预构建

Vite使用esbuild在初次启动开发服务器前把检测到的依赖进行预构建。Vite 基于ESM,在使用某些模块时,由于模块依赖了另一些模块,依赖的模块又基于另一些模块。会出现页面初始化时一次发送数百个模块请求的情况。

以 lodash-es 为例,代码中以 import { debounce } from 'lodash' 导入一个命名函数时候,并不是只下载包含这个函数的文件,而是有一个依赖图。

image.png

可以看到一共发送了651个请求。一共花费1.53s。

Vite 为了优化这个情况,利用esbuild在启动的时候预先把 debounce 用到的所有内部模块全部打包成一个 bundle,这样就浏览器在请求 debounce 时,便只需要发送一次请求了。预构建后,只发送了14个请求。所以预构建的优势可想而知。

官网
  • 兼容其他模块发规范:==开发阶段中==,Vite 的开发服务器将所有代码视为原生 ES 模块。因此,Vite 必须先将作为 CommonJS 或 UMD 发布的依赖项转换为 ESM。

  • 性能优化 : Vite 将有许多内部模块的 ESM 依赖关系转换为单个模块,以提高后续页面加载性能。

解读
原因:简单来讲就是为了提高本地开发服务器的冷启动速度。按照vite的说法,当冷启动开发服务器时,基于打包器的方式启动必须优先抓取并构建你的整个应用,然后才能提供服务。随着应用规模的增大,打包速度显著下降,本地服务器的启动速度也跟着变慢。
解决方式:为了加快本地开发服务器的启动速度,vite 引入了预构建机制。在预构建工具的选择上,vite选择了 Esbuild 。Esbuild 使用 Go 编写,比以 JavaScript 编写的打包器构建速度快 10-100倍,有了预构建,再利用浏览器的esm方式按需加载业务代码,动态实时进行构建,结合缓存机制,大大提升了服务器的启动速度。

预构建流程

第一步 查找依赖

==如果是首次启动本地服务==,那么vite会自动抓取源代码,从代码中找到需要预构建的依赖

  • 调用 Esbuild 的 Build Api ,以index.html作为查找入口(entryPoints),将所有的来自node_modules以及在配置文件的optimizeDeps.include选项中指定的模块找出来

==如果不是首次启动==

  • 首先会去查找缓存目录(默认是 node_modules/.vite)下的 _metadata.json 文件;然后找到当前项目依赖信息(xxx-lock 文件)拼接上部分配置后做哈希编码,最后对比缓存目录下的 hash 值是否与编码后的 hash 值一致,一致并且没有开启 force 就直接返回预构建信息,结束整个流程;

第二步 对查找到的依赖进行构建

  • 已经得到了需要预构建的依赖列表。现在需要把他们作为 Esbuild 的 entryPoints 打包就行了。
  • vite 并没有将 Esbuild 的 outdir(构建产物的输出目录)直接配置为.vite目录,而是先将构建产物存放到了一个临时目录。当构建完成后,才将原来旧的.vite(如果有的话)删除。然后再将临时目录重命名为.vite。这样做主要是为了避免在程序运行过程中发生了错误,导致缓存不可用。

预构建的缓存策略

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

文件系统缓存

image.png

Vite 会将预构建的依赖缓存到 node_modules/.vite。它根据几个源来决定是否需要重新运行预构建步骤:

  • package.json 中的 dependencies 列表
  • 包管理器的 lockfile,例如 package-lock.json,yarn.lock,或者 pnpm-lock.yaml
  • 可能在 vite.config.js 相关字段中配置过的

只有在上述其中一项发生更改时,才需要重新运行预构建。

如果出于某些原因,你想要强制 Vite 重新构建依赖,你可以用 --force 命令行选项启动开发服务器,或者手动删除 node_modules/.vite 目录。

浏览器缓存

image.png

意味着这些依赖的过期时间是(即为max-age所能设置的最大值):一年

解析后的依赖请求会以 HTTP 头 max-age=31536000,immutable 强缓存,以提高在开发时的页面重载性能。一旦被缓存,这些请求将永远不会再到达开发服务器。如果安装了不同的版本(这反映在包管理器的 lockfile 中),则附加的版本 query 会自动使它们失效。

总结

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

静态资源加载

当请求的路径符合 imageRE, mediaRE, fontsRE 或 JSON 格式,会被认为是一个静态资源。静态资源将处理成ESM模块返回。

// src/node/utils/pathUtils.ts
const imageRE = /\.(png|jpe?g|gif|svg|ico|webp)(\?.*)?$/
const mediaRE = /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/
const fontsRE = /\.(woff2?|eot|ttf|otf)(\?.*)?$/i
export const isStaticAsset = (file: string) => {
  return imageRE.test(file) || mediaRE.test(file) || fontsRE.test(file)
}

// src/node/server/serverPluginAssets.ts
app.use(async (ctx, next) => {
  if (isStaticAsset(ctx.path) && isImportRequest(ctx)) {
    ctx.type = 'js'
    ctx.body = export default ${JSON.stringify(ctx.path)} // 输出是path
    return
  }
  return next()
})

export const jsonPlugin: ServerPlugin = ({ app }) => {
  app.use(async (ctx, next) => {
    await next()
    // handle .json imports
    // note ctx.body could be null if upstream set status to 304
    if (ctx.path.endsWith('.json') && isImportRequest(ctx) && ctx.body) {
      ctx.type = 'js'
      ctx.body = dataToEsm(JSON.parse((await readBody(ctx.body))!), {
        namedExports: true,
        preferConst: true
      })
    }
  })
}
复制代码

js/ts处理

Vite使用 esbuild 将 ts 转译到 js,约是tsc速度的20~30倍,同时HMR更新反应到浏览器的时间会小于50ms。但是,由于esbuild转换ts到js对于类型操作仅仅是擦除,所以完全保证不了类型正确,因此需要额外校验类型,比如使用tsc --noEmit。 将ts转换成js后,浏览器便可以利用ESM直接拿到js资源。

Vite Hmr

提及「HMR」,不可避免地是会想起现在我们家喻户晓的 webpack-dev-server 中的「HMR」。而在 webpack-dev-server 中实现「HMR」的核心就是 HotModuleReplacementPlugin ,它是「Webpack」内置的「Plugin」。在我们平常开发中,之所以改一个文件,例如 .vue 文件,会触发「HMR」,是因为在 vue-loader 中已经内置了使用 HotModuleReplacementPlugin 的逻辑。

整体流程

Vite 的热加载原理,其实就是在客户端与服务端建立了一个 websocket 连接,当代码被修改时,服务端发送消息通知客户端去请求修改模块的代码,完成热更新。

  • 服务端:服务端做的就是监听代码文件的改变,在合适的时机向客户端发送 websocket 信息通知客户端去请求新的模块代码。
  • 客户端:Vite 中客户端的 websocket 相关代码在处理 html 中时被写入代码中。可以看到在处理 html 时,vite/client 的相关代码已经被插入。

简而言之,Vite 在启动之前会创建一个为热更新服务定制的 websocket 服务器,然后对项目文件进行监听。同时客户端的 html 里注入了 @vite/client 来与服务端进行配合实现热更新。

image.png

HMR API

interface ImportMeta {
  readonly hot?: {
    readonly data: any

    accept(): void
    accept(cb: (mod: any) => void): void
    accept(dep: string, cb: (mod: any) => void): void
    accept(deps: string[], cb: (mods: any[]) => void): void

    prune(cb: () => void): void
    dispose(cb: (data: any) => void): void
    decline(): void
    invalidate(): void

    on(event: string, cb: (...args: any[]) => void): void
  }
}
复制代码

其中一个重点 (大家一起理解一下这里,后面会存在计算 HMR 边界)

  • hot.accept(cb) 要接收模块自身,应使用 import.meta.hot.accept,参数为接收已更新模块的回调函数:
export const count = 1

if (import.meta.hot) {
  import.meta.hot.accept((newModule) => {
    console.log('updated: count is now ', newModule.count)
  })
}
复制代码

==“接受” 热更新的模块被认为是 HMR 边界==。

请注意,Vite 的 HMR 实际上并不替换最初导入的模块:如果 HMR 边界模块从某个依赖重新导出其导入,则它应负责更新这些重新导出的模块(这些导出必须使用 let)。此外,从边界模块向上的导入者将不会收到更新。

Vite Hmr Server

监听变化 触发HMR

当文件修改后,会触发文件监听实例 watcher 回调
监听文件变更类型
// 文件改变时触发事件
watcher.on('change', async (file) => {
  // 规范化文件路径,将\\替换成/
  file = normalizePath(file)
  // ...
})

// 添加文件事件
watcher.on('add', (file) => {
  handleFileAddUnlink(normalizePath(file), server)
})

// 删除文件事件
watcher.on('unlink', (file) => {
  handleFileAddUnlink(normalizePath(file), server, true)
})
复制代码

回调中拿到文件路径 file 会进行 normalizePath,他的作用是规范化文件路径,将 \\ 替换成 /

假设我们对文件进行修改 change, 下面的代码逻辑是这样的

// 文件改变时触发事件
watcher.on('change', async (file) => {
  // 规范化文件路径,将\\替换成/
  file = normalizePath(file)
  if (file.endsWith('/package.json')) {
    return invalidatePackageData(packageCache, file)
  }
  // invalidate module graph cache on file change
  moduleGraph.onFileChange(file)
  if (serverConfig.hmr !== false) {
    try {
      await handleHMRUpdate(file, server)
    } catch (err) {
      ws.send({
        type: 'error',
        err: prepareError(err)
      })
    }
  }
})
复制代码

这里我们看到了 onFileChange 以及 handleHMRUpdate

onFileChange
onFileChange(file: string): void {
  // 根据文件获取模块信息
  const mods = this.getModulesByFile(file)
  if (mods) {
    const seen = new Set<ModuleNode>()
    mods.forEach((mod) => {
      this.invalidateModule(mod, seen)
    })
  }
}

// 处理模块
invalidateModule(mod: ModuleNode, seen: Set<ModuleNode> = new Set()): void {
  mod.info = undefined
  mod.transformResult = null
  mod.ssrTransformResult = null
  // ...
}
复制代码

其实看到这里很纳闷,为什么要将改变的模块里面的状态都设置为 null ,思考了好久,直到看到一行注释并打开了有道翻译

image.png 原来,他是想将改变的模块进行初始化的意思,然后再去走热更新流程。

开始热更新

更新模块信息和计算 HMR 边界
handleHMRUpdate
export async function handleHMRUpdate(
  file: string,
  server: ViteDevServer
): Promise<any> {
  const { ws, config, moduleGraph } = server
  
  const shortFile = getShortName(file, config.root)

  // 配置文件修改,比如 vite.config.ts
  const isConfig = file === config.configFile
  // 配置文件的依赖
  const isConfigDependency = config.configFileDependencies.some(
    (name) => file === path.resolve(name)
  )
  // 环境变量文件
  const isEnv =
    config.inlineConfig.envFile !== false &&
    (file === '.env' || file.startsWith('.env.'))

  // 如果是配置文件修改了,直接重启服务
  if (isConfig || isConfigDependency || isEnv) {
    // auto restart server
    try {
      await server.restart()
    } catch (e) {
      config.logger.error(colors.red(e))
    }
    return
  }

  // vite 的 client 修改了,全量刷新 -> 刷新页面
  if (file.startsWith(normalizedClientDir)) {
    ws.send({
      type: 'full-reload',
      path: '*'
    })
    return
  }

  // 获取文件关联的模块
  const mods = moduleGraph.getModulesByFile(file)

  // check if any plugin wants to perform custom HMR handling
  const timestamp = Date.now()
  // 热更上下文
  const hmrContext: HmrContext = {
    // 文件
    file,
    // 时间戳
    timestamp,
    // 受更改文件影响的模块数组
    modules: mods ? [...mods] : [],
    // 这是一个异步读函数,它返回文件的内容。之所以这样做,是因为在某些系统上,文件更改的回调函数可能会在编辑器完成文件更新之前过快地触发
    // 并 fs.readFile 直接会返回空内容。传入的 read 函数规范了这种行为。
    read: () => readModifiedFile(file),
    // 整个服务对象
    server
  }

  // 遍历插件,调用 handleHotUpdate 钩子
  for (const plugin of config.plugins) {
    if (plugin.handleHotUpdate) {
      const filteredModules = await plugin.handleHotUpdate(hmrContext)

      // 受更改文件影响的模块数组
      if (filteredModules) {
        hmrContext.modules = filteredModules
      }
    }
  }

  // 文件修改没有影响其他模块
  if (!hmrContext.modules.length) {
    // 是 html 的话,直接刷新页面
    if (file.endsWith('.html')) {
      ws.send({
        type: 'full-reload',
        path: config.server.middlewareMode
          ? '*'
          : '/' + normalizePath(path.relative(config.root, file))
      })
    }
    return
  }

  // 核心,执行模块更新
  updateModules(shortFile, hmrContext.modules, timestamp, server)
}
复制代码

handleHMRUpdate 主要处理了:

  • 如果修改的是 vite.config.ts 或它的依赖文件,亦或者是环境变量的定义文件,都直接重启服务;
  • 如果修改的是 vite 自带的 client 脚本,就刷新页面;
  • 如果上述两种情况都不是,就定义 hmrContext 对象, 定义包含了 file 当前文件路径、timestamp 当前时间戳、modules 文件映射的模块、read 函数读取该文件内容、server 整个服务器对象;有了 hmrContext 之后,依次调用插件的 handleHotUpdate 钩子,钩子可以返回热更需要关联的模块,具体可以查看官方 HMR API 。如果没有关联的模块,并且修改的是 html 文件,发送 full-reload 进行页面刷新;前面几个条件都不满足的话,就调用 updateModules 。
updateModules

updateModulespropagateUpdate 的源码就不在这里展示了,我们只看其作用:

updateModules 会遍历 modules,调用 invalidate 更新模块和引用者(importers)的信息,声明 HMR 边界(“接受” 热更新的模块),调用 propagateUpdate 判断模块之前是否存在“死路”,如果存在“死路”就直接发起 full-reload 命令刷新页面,否则发起 update 命令执行指定模块(updates)的更新。

Vite Hmr Server 运行流程

image.png

Vite Hmr Client

个人觉得 Client 端逻辑相比于 Server 端会更加清晰,便于总结。

image.png

引入 @vite/client 脚本后,会初始化 websocket 连接,收到数据时会触发 handleMessage 事件。

我们只对其中 update 处理逻辑做个介绍

case 'update':
  notifyListeners('vite:beforeUpdate', payload)

  if (isFirstUpdate && hasErrorOverlay()) {
    window.location.reload()
    return
  } else {
    clearErrorOverlay()
    isFirstUpdate = false
  }
  payload.updates.forEach((update) => {
    // 以 js  文件更新举例
    if (update.type === 'js-update') {
      queueUpdate(fetchUpdate(update))
    } else {
      //...
    }
  })
  break
复制代码

我们着重去分析一个 fetchUpdate 这个方法

async function fetchUpdate({ path, acceptedPath, timestamp }: Update) {
  const mod = hotModulesMap.get(path)
  if (!mod) {
    return
  }

  const moduleMap = new Map()
  const isSelfUpdate = path === acceptedPath

  // make sure we only import each dep once
  const modulesToUpdate = new Set<string>()
  if (isSelfUpdate) {
    // self update - only update self
    modulesToUpdate.add(path)
  } else {
    // dep update
    for (const { deps } of mod.callbacks) {
      deps.forEach((dep) => {
        if (acceptedPath === dep) {
          modulesToUpdate.add(dep)
        }
      })
    }
  }

  // determine the qualified callbacks before we re-import the modules
  const qualifiedCallbacks = mod.callbacks.filter(({ deps }) => {
    return deps.some((dep) => modulesToUpdate.has(dep))
  })

  await Promise.all(
    Array.from(modulesToUpdate).map(async (dep) => {
      const disposer = disposeMap.get(dep)
      if (disposer) await disposer(dataMap.get(dep))
      const [path, query] = dep.split(`?`)
      try {
        const newMod = await import(
          /* @vite-ignore */
          base +
            path.slice(1) +
            `?import&t=${timestamp}${query ? `&${query}` : ''}`
        )
        moduleMap.set(dep, newMod)
      } catch (e) {
        warnFailedFetch(e, dep)
      }
    })
  )

  return () => {
    for (const { deps, fn } of qualifiedCallbacks) {
      fn(deps.map((dep) => moduleMap.get(dep)))
    }
    const loggedPath = isSelfUpdate ? path : `${acceptedPath} via ${path}`
    console.log(`[vite] hot updated: ${loggedPath}`)
  }
}
复制代码
解读:根据 path 从 hotModulesMap 中获取模块信息,如果模块不存在就直接终止,存在则判断是否“自我接受”,是的话就把自己加入到待更新集合,否则就去遍历全部 deps 加入到更新集合并重新获取回调函数。之后遍历待更新队列 modulesToUpdate,如果模块有 dispose 函数的定义就清除副作用。再就是来到最核心的地方,通过动态 import 去加载最新的资源并更新模块信息,保证最后的回调拿到的模块是最新的。

image.png

从上图可以看到,每次修改文件都会用最新的时间戳去请求资源。fetchUpdate 最后返回一个函数,通过 queueUpdate 保证回调函数的执行顺序跟 http 请求的一致。

let pending = false
let queued: Promise<(() => void) | undefined>[] = []

async function queueUpdate(p: Promise<(() => void) | undefined>) {
  // 先将全部回调函数推到 queued
  queued.push(p)
  if (!pending) {
    pending = true
    await Promise.resolve()
    pending = false
    const loading = [...queued]
    queued = []
    ;(await Promise.all(loading)).forEach((fn) => fn && fn())
  }
}
复制代码
至此,Vite Hmr 实现流程圆满结束!

我为什么要用 Vite ?

通过上文几个特性的介绍,大姐应该最先脱口而出的一点,那就是极致的开发反应速度(从启动到调试)

WebpackVite
先打包生成bundle,再启动开发服务器先启动开发服务器,利用新一代浏览器的ESM能力,无需打包,直接请求所需模块并实时编译
HMR时需要把改动模块及相关依赖全部编译HMR时只需让浏览器重新请求该模块,同时利用浏览器的缓存(源码模块协商缓存,依赖模块强缓存)来优化请求
因为: Vite 不需要像 Webpack 启动后会做一堆事情,经历一条很长的编译打包链条,从入口开始需要逐步经历语法解析、依赖收集、代码转译、打包合并、代码优化,最终将高版本的、离散的源码编译打包成低版本、高兼容性的产物代码。
而是: 在开发环境冷启动无需打包,无需分析模块之间的依赖,同时也无需在启动开发服务器前进行编译,启动时还会使用esbuild来进行预构建。
并且: Vite 只需执行初始化命令,就可以得到一个预设好的开发环境,开箱即获得一堆功能,包括:CSS预处理、html预处理、异步加载、分包、压缩、HMR等。他使用复杂度介于Parcel和Webpack的中间,只是暴露了极少数的配置项和plugin接口,既不会像Parcel一样配置不灵活,又不会像Webpack一样需要了解庞大的loader、plugin生态。

注意的是

在生产环境,由于嵌套导入会导致发送大量的网络请求,即使使用 HTTP2.x(多路复用、首部压缩),在生产环境中发布未打包的ESM仍然性能低下。因此,对比在开发环境Vite使用esbuild来构建依赖,生产环境Vite则使用了更加成熟的Rollup来完成整个打包过程。==因为esbuild虽然快,但针对应用级别的代码分割、CSS处理仍然不够稳定,同时也未能兼容一些未提供ESM的SDK。==

为了在生产环境中获得最佳的加载性能,仍然需要对代码进行tree-shaking、懒加载以及chunk分割(以获得更好的缓存)。

总结

到这里,本人的《从 Webpack 到 Vite》系列就完结了,在梳理文章的时候看了无数篇无数遍大佬的文章和官网的解释,也花了很多时间去看平时基本不会看的源码,尽量给大家整理成通俗易懂的形式。文章中可能有很多概念个人也是只顾着逻辑的串通而并没有清晰的理解含义,但对个人来讲也有些许收获,也希望能帮助大家进一步了解 Webpack 和 Vite, 谢谢!😘😘😘

分类:
前端
标签:
收藏成功!
已添加到「」, 点击更改