再聊Vite,极简原理和实践

756 阅读12分钟

前言

vite的出现原因就不多赘述了,就是模块化的发展响应的产物。

如果说,webpack的出现是为了解决模块化打包的问题,那vite的出现可以说是,借助了官方的模块化规范的基础上去实现更好的开发体验。 这里我们再简单回顾下模块化发展:

JavaScript 在最初并没有模块化的概念,但随着前端业务复杂度的增加,模块化逐渐变得至关重要。于是,社区涌现了多种模块化方案,这些方案在相互借鉴的同时也引发了许多争议,形成了多个流派。从最初的 CommonJSES6 推出的 ES Modules 规范,经过不断的讨论和演变,最终 ES Modules 成为前端开发的重要基础设施。

  • CommonJS:现主要用于Node.js(Node@13.2.0开始支持直接使用ES Module)
  • AMDrequire.js 依赖前置,市场存量不建议使用
  • CMDsea.js 就近执行,市场存量不建议使用
  • ES Module:ES语言规范,标准,趋势,未来

那由于 ES Module 是未来,是趋势,是大家都共同去遵循的规范,也就是现代浏览器都支持。在此基础上,更好的打包工具 vite 诞生,Vite依靠浏览器对 ES Module 规范的实现来提升开发阶段的体验和效率。

为什么需要vite?vite解决什么问题?

这几年,打包构建工具层出不穷,我们比较熟悉的有gulp rollup webpack vite 等等,但下载量最大的还是webpack。

那在vite出现之前,这些打包构建工具都有什么问题亟待解决呢?

当前常见的构建工具如 Webpack,主要通过抓取、编译和构建整个应用的代码(即打包过程),生成一份经过编译和优化,能够良好兼容各大浏览器的生产环境代码。在开发环境中,流程大体相同:首先将整个应用进行构建打包,然后将打包后的代码交给 dev server(开发服务器)进行处理。

Webpack 等构建工具的出现极大地简化了前端开发流程,但随着前端业务的复杂性增加,JS 代码量呈指数增长,打包构建时间也不断加长,导致 dev server(开发服务器)在性能上遇到了瓶颈:

  • 服务启动缓慢:在大型项目中,dev server 启动时间可能长达几十秒甚至几分钟。
  • HMR 热更新缓慢:即便启用了 HMR 模式,随着应用规模的增加,热更新速度也显著变慢,性能瓶颈日益显现,优化空间有限。

缓慢的开发环境显著降低了开发者的工作体验和效率,在这种背景下,Vite 应运而生。

什么是vite?

就是在开发环境依赖 esbuild,生产环境依赖 rollup,同时借助浏览器ESM的编译功能实现开发效率大幅提升的新一代打包构建工具。

说人话:就是相比别的打包工具,开发环境的编译快的一批,因为它不需要打包了。

先介绍一些概念:

  • 依赖:指的是开发过程中不会频繁变动的部分(如 npm 包、UI 组件库等),这些部分会通过 esbuild 进行预构建。
  • 源码:指的是浏览器无法直接执行的非 JavaScript 代码(如 .jsx、.css、.vue 等文件)。Vite 只会在浏览器请求相关源码时进行转换,提供 ESM 格式的源码。

在开发环境: 利用浏览器原生的 ESM(ES Modules)编译能力,Vite 省去了繁琐的编译过程,直接将开发环境的源码交给浏览器。dev server 只需提供轻量级的服务。在浏览器执行 ESM 的 import 时,它会向 dev server 发起 AJAX 请求,服务器会对源码进行简单处理后返回给浏览器。

Vite 的热模块替换(HMR)是基于原生 ESM 实现的。当我们编辑一个文件时,Vite 只需精准地使已编辑的模块失效,从而确保无论应用多大,HMR 始终能够保持快速更新。

通过使用 esbuild 来处理项目的依赖,esbuild 是用 Go 语言编写的,比基于 Node.js 的编译器速度更快几个数量级。

在生产环境: Vite 集成了 Rollup 来打包生产环境代码,依赖 Rollup 生态系统的成熟稳定性以及其简洁的插件机制。

优势和不足:

优势

  • 极速开发体验: 快速启动、快速加载、快速更新!
  • 开箱即用: 高度集成,无需额外配置即可投入使用。
  • 高效热更新: 基于原生 ESM,实现无需打包编译的极速热更新。
  • 依赖预处理: 采用 esbuild 进行依赖预构建,性能相比基于 Node.js 的编译器(如 Webpack)提升了几个数量级。
  • 插件支持: 兼容 Rollup 的插件生态,插件开发更加简洁高效。
  • 框架无关性: 虽然对 Vue 支持最佳,但同样支持 React 等其他框架,是一个独立的构建工具。
  • 内置功能丰富: 提供开箱即用的 SSR 支持,并且对 TypeScript 具备天然支持。

不足

  • Vue 优先支持: 由于为 Vue 量身打造,编译插件的优化主要针对 Vue,对 React 的支持稍显逊色。
  • 生产环境实践较少: 尽管 2.0 正式版已发布并可用于生产环境,但市场实际使用案例相对较少。
  • 开发与生产的不一致: 开发环境基于原生 ESM,生产环境集成 Rollup 打包,导致开发环境与最终生产代码执行逻辑存在差异。

vite运行流程

Webpack 是完整打包整个应用,再把打包后的代码产物提供给 dev server,这样浏览器才能访问 dev server 的资源。

image.png

Vite 将源码直接交付给浏览器,使开发服务器 dev server 能够秒级启动。当浏览器需要加载某个模块以显示页面时,会向开发服务器发起请求。服务器 dev server 对该模块进行简单处理后(编译处理.vue .jsx的文件资源),将其返回给浏览器,从而实现真正的按需加载。

image.png

Vite 的处理流程可以分为两大阶段:开发环境生产环境。下面是 Vite 在这两个阶段的详细处理流程:

开发环境处理流程

在开发环境中,Vite 主要依靠浏览器原生的 ESM(ES Modules) 特性,避免了传统构建工具中的繁重编译和打包过程。具体步骤如下:

1. 启动 Vite 开发服务器
  • 启动 Vite 时,Vite 启动一个开发服务器 dev server,通过该服务器提供给浏览器需要的源码。
  • 开发服务器并不直接提供所有文件,而是采用按需加载的方式,确保开发过程的快速响应。
2. 请求模块
  • 当浏览器执行 importexport 时,浏览器会发送 ESM 请求dev server,请求某个模块的源码(例如 .js.ts.vue 等文件)。
  • 请求的模块可能包含依赖,这些依赖会继续通过 dev server 被动态请求。
3. 处理源码
  • 非 JavaScript 代码:(如 .jsx.css.vue 等),Vite 会在浏览器请求相关文件时进行即时转换。这种转换是由 Vite 内置的 esbuild 实现的,esbuild 会快速将这些文件转换为浏览器可以执行的 ESM 格式。

  • 依赖:对于项目中的静态依赖(如 npm 包或 UI 组件库),Vite 会利用 esbuild 在启动时进行预构建,将这些依赖转换为兼容的 ESM 格式。

4. 模块热更新 (HMR)
  • 当开发者修改文件时,Vite 会触发 热模块替换(HMR) 。由于 Vite 基于原生 ESM 实现 HMR,只会重新加载发生变化的模块,而不是整个应用。
  • Vite 在模块级别进行精准更新,这意味着它只会使编辑过的模块失效,确保更新保持快速,无论应用的大小。
5. 使用 esbuild 处理依赖
  • Vite 使用 esbuild 进行项目依赖的处理,esbuild 作为一个基于 Go 的编译器,比传统的基于 Node.js 的编译器速度快上几个数量级。它用于预构建依赖,以提高启动速度和模块加载效率。

生产环境处理流程

在生产环境中,Vite 主要依赖 Rollup 来处理代码的打包和优化。具体步骤如下:

1. 集成 Rollup 打包
  • 当你准备打包生产环境代码时,Vite 使用 Rollup 来进行最终的代码打包。Rollup 是一个非常高效的 JavaScript 打包工具,尤其适合处理模块化的代码。
  • Rollup 会优化并合并所有的模块,生成一个最终的生产环境代码,这些代码可以在各种浏览器中高效运行。
2. 代码优化
  • 在打包过程中,Vite 会进行多种优化,例如:

    • 树摇(Tree Shaking) :移除无用代码,减少最终打包后的文件体积。
    • 代码分割(Code Splitting) :将应用分割成多个更小的文件,按需加载,提高加载速度。
    • 插件处理:Vite 利用 Rollup 插件机制进行进一步的代码优化、压缩和兼容性处理。
3. 生成生产环境代码

最终,Vite 会生成经过优化和打包后的生产环境代码,通常包括:

  • 压缩后的 JavaScript 文件。
  • 优化后的静态资源(如 CSS、图片等)。
  • 可以直接在浏览器中高效运行的代码。

在开发环境,Vite 利用浏览器原生的 ESM 和 esbuild,省去了繁琐的打包步骤,直接为浏览器提供原生的模块化代码,支持按需加载和快速的热模块替换(HMR)。而在生产环境:Vite 集成了 Rollup 进行最终的代码打包,进行多种优化,如代码分割、树摇、插件支持等,生成适合生产环境的高效代码。

这个流程的关键是 Vite 的开发环境通过浏览器原生的 ESM 机制来实现快速开发,而生产环境则依赖 Rollup 进行高效的打包和优化

怎么使用vite?

废话不多说,直接上配置

vite.config.js

import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue'; // 示例中使用 Vue 框架
import viteCompression from 'vite-plugin-compression'; // 用于 gzip 压缩
import path from 'path';

export default defineConfig({
  // 项目根目录
  root: './',
  
  // 插件配置
  plugins: [
    vue(),
    viteCompression({
      algorithm: 'gzip', // 使用 gzip 压缩
      threshold: 10240, // 超过 10KB 的文件才会压缩
      ext: '.gz', // 压缩文件的扩展名
    }),
  ],

  // 开发服务器配置
  server: {
    port: 3000, // 指定开发服务器端口
    open: true, // 自动打开浏览器
    hmr: {
      overlay: false, // 禁用错误的全屏覆盖提示
    },
  },

  // 依赖优化配置
  optimizeDeps: {
    include: ['axios', 'lodash'], // 强制预构建的依赖
    exclude: ['some-large-lib'], // 排除不需要预构建的依赖
  },

  // 静态资源处理
  assetsInclude: ['**/*.gltf', '**/*.glb'], // 自定义资源类型
  build: {
    target: 'esnext', // 使用现代浏览器的最新语法
    minify: 'esbuild', // 使用 esbuild 进行压缩,速度更快
    sourcemap: false, // 关闭生产环境的 sourcemap
    assetsInlineLimit: 4096, // 小于 4KB 的资源内联为 Base64

    // Rollup 相关配置
    rollupOptions: {
      output: {
        manualChunks(id) {
          if (id.includes('node_modules')) {
            return 'vendor'; // 将第三方依赖打包到 vendor.js
          }
        },
        entryFileNames: 'assets/[name].[hash].js',
        chunkFileNames: 'assets/[name].[hash].js',
        assetFileNames: 'assets/[name].[hash].[ext]',
      },
    },
  },

  // 路径别名
  resolve: {
    alias: {
      '@': path.resolve(__dirname, './src'), // 配置 @ 指向 src 目录
    },
  },
});

配置详解

插件配置

  • Vue 插件:支持 Vue.js 的开发。
  • Gzip 压缩插件:压缩静态文件,减小生产环境的文件大小。

开发服务器优化

  • 指定端口、自动打开浏览器。
  • 禁用全屏错误覆盖,避免影响开发体验。

依赖预构建

  • include:指定需要提前预构建的依赖库(如常用的第三方库)。
  • exclude:排除某些大型库的预构建,按需加载。

静态资源处理

  • 设置小于 4KB 的资源内联到 JavaScript 中。
  • 指定自定义资源类型(如 3D 模型文件)。

生产环境优化

  • 使用 esbuild 进行代码压缩,提升构建速度。
  • 关闭生产环境的 sourcemap,减小构建输出体积。
  • 代码分割:将第三方依赖打包到单独的 vendor.js 中。

路径别名

配置 @ 为路径别名,方便在代码中引用 src 目录下的文件。

使用上面的配置可以达到的优化效果:

开发环境:预构建依赖提升启动速度;按需加载模块加快页面加载;开发服务器配置提高开发体验。

生产环境:压缩和代码分割减少文件体积; 静态资源处理优化加载效率;文件名加哈希实现缓存优化。

核心原理

ESbuild 编译

esbuild 使用go编写,cpu密集下更具性能优势,编译速度更快,以下摘自官网的构建速度对比:

image.png

依赖预构建

  • 模块化兼容: 如前文所述,目前依然存在多种模块化规范并存的情况。在预构建阶段,Vite会将依赖中使用的各种模块化标准(如 CommonJS、UMD)转换为 ESM 格式,方便浏览器直接加载。

  • 性能优化: 许多 npm 包内部使用 ESM 格式,含有大量的 import 请求,这可能导致网络请求过多而造成拥堵。Vite 借助 esbuild 工具,将这些复杂的 ESM 依赖关系打包成单一模块,减少网络请求次数,从而提升加载性能。

按需加载

  • 服务器仅在收到 import 请求时才会编译对应文件,并将 ESM 格式的源码返回给浏览器,从而实现真正的按需加载。

缓存

  • HTTP 缓存: 为优化性能,充分利用 HTTP 缓存机制。对依赖(通常不会频繁更改的代码)启用 max-ageimmutable 的强缓存策略,而对源码部分采用 304 协商缓存策略,有效提升页面加载速度。
  • 文件系统缓存: 在预构建阶段,Vite 会将构建后的依赖缓存至 node_modules/.vite 目录。只有在相关配置更改或手动触发时才会重新构建,进一步加快预构建的速度。

模块路径重写

浏览器的 import 语法仅支持相对路径或绝对路径,而开发中通常通过包名直接引入 node_modules 中的模块,因此需要进行路径转换,使浏览器能够正确加载。

  • 使用 es-module-lexer 扫描并解析 import 语句。
  • 利用 magic-string 对模块路径进行重写,确保兼容性和正确性。
// 开发代码 
import { createApp } from 'vue' 

// 转换后 
import { createApp } from '/node_modules/vue/dist/vue.js'