2022-03-03
前端工程化
00

目录

1. 背景介绍
1.1 什么是 Vite ?
1.2 Vite 诞生的背景
2. 环境搭建
3. Vite 插件
3.1 使用 vite 插件(@vitejs/plugin-legacy)
3.2 使用 rollup 插件(rollup-plugin-visualizer)
3.3 强制插件排序
3.4 按需加载插件
3.5 自定义插件
3.6 vite 和 rollup 钩子函数
4 实现原理
4.1 对依赖的处理
4.2 对源码的处理
5. vite 和 webpack 的区别

1. 背景介绍

1.1 什么是 Vite ?

Vitevue 的作者尤雨溪在开发 vue3.0 的时候开发的一个 web 开发构建工具。由于其原生 ES 模块导入方式,可以实现闪电般的冷服务器启动。Vite 是一种新型前端构建工具,能够显著提升前端开发体验。它主要由两部分组成:

  1. 一个开发服务器,它基于 原生 ES 模块 提供了 丰富的内建功能,如速度快到惊人的 模块热更新(HMR)
  2. 一套构建指令,它使用 Rollup 打包你的代码,并且它是预配置的,可输出用于生产环境的高度优化过的静态资源。

Vite 意在提供开箱即用的配置,同时它的 插件 APIJavaScript API 带来了高度的可扩展性,并有完整的类型支持。

1.2 Vite 诞生的背景

这里的背景介绍会从与 Vite 紧密相关的两个概念的发展史说起

  1. 一个是 JavaScript 的模块化标准
  2. 另一个是前端构建工具

共存的模块化标准

为什么 JavaScript 会有多种共存的模块化标准?因为 js 在设计之初并没有模块化的概念,随着前端业务复杂度不断提高,模块化越来越受到开发者的重视,社区开始涌现多种模块化解决方案,它们相互借鉴,也争议不断,形成多个派系,从 CommonJS 开始,到 ES6 正式推出 ES Modules 规范结束,所有争论,终成历史,ES Modules 也成为前端重要的基础设施。

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

Vite 的核心正是依靠浏览器对 ES Module 规范的实现。

发展中的构建工具

近些年前端工程化发展迅速,各种构建工具层出不穷,目前 Webpack 仍然占据统治地位,npm 每周下载量达到两千多万次。下面是开发者比较熟知的一些构建工具

image.png

痛点

现在常用的构建工具如 Webpack,主要是通过 抓取-编译-构建 整个应用的代码(也就是常说的打包过程),生成一份编译、优化后能良好兼容各个浏览器的的生产环境代码。在开发环境流程也基本相同,需要先将整个应用构建打包后,再把打包后的代码交给 dev server(开发服务器)。 Webpack 等构建工具的诞生给前端开发带来了极大的便利,但随着前端业务的复杂化,js代码量呈指数增长,打包构建时间越来越久,dev server(开发服务器)性能遇到瓶颈:

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

缓慢的开发环境,大大降低了开发者的幸福感,在以上背景下 Vite 应运而生。

2. 环境搭建

项目创建(以 vue 项目为例)

  1. 安装依赖
sh
npm init vite@latest
  1. 输入项目名称,选择项目模版

image.png

  1. 模版安装完成后进入项目安装依赖并启动项目,一个简单的 vue 模版就创建完成了

image.png

  • vitevue-cli 对比
  1. Vite 在开发环境下速度快,体验好,但是只针对 ES6 浏览器。vue-cli创建的项目开发速度慢,体验差,可以针对旧浏览器;
  2. Vite 不支持 vue2,所以不能选择版本,是直接构建 vue3vue-cli 创建项目可以选择版本;
  3. Vite 创建的项目没有集成 vue-routervuex 等插件,还需要手动安装;
  4. vue-cli 作为老牌构建工具,使用者众多,更加经得住历史的考验,并且得益于使用者众多,所以在生态环境和插件数量方面更好。

3. Vite 插件

Vite 可以使用插件进行扩展,这得益于 Rollup 优秀的插件接口设计和一部分 Vite 独有的额外选项。这意味着 Vite 用户可以利用 Rollup 插件的强大生态系统,同时根据需要也能够扩展开发服务器和 SSR 功能。

3.1 使用 vite 插件(@vitejs/plugin-legacy)

  1. 安装依赖

@vitejs/plugin-legacy: 为打包后的文件提供传统浏览器兼容性支持

sh
npm i -D @vitejs/plugin-legacy
  1. vite.config.ts 引入模块并添加配置
ts
import legacy from '@vitejs/plugin-legacy' export default defineConfig({ plugins: [ legacy({ targets: ['defaults', 'not IE 11'] }) ] })

3.2 使用 rollup 插件(rollup-plugin-visualizer)

  1. 安装依赖

rollup-plugin-visualizer: 构建分析,打包之后,会在根目录默认生成一个 stats.html 文件

sh
npm install rollup-plugin-visualizer
  1. vite.config.ts 中引入模块并添加配置
ts
import { defineConfig } from "vite"; import vue from "@vitejs/plugin-vue"; import legacy from "@vitejs/plugin-legacy"; import { visualizer } from "rollup-plugin-visualizer"; // https://vitejs.dev/config/ export default defineConfig({ plugins: [ vue(), legacy({ targets: ["defaults", "not IE 11"], }), visualizer() ], });
  1. 执行打包命令后,在浏览器中打开 stats.html 文件

image.png

3.3 强制插件排序

为了与某些 Rollup 插件兼容,可能需要强制执行插件的顺序,或者只在构建时使用。这应该是 Vite 插件的实现细节。可以使用 enforce 修饰符来强制插件的位置:

  • pre:在 Vite 核心插件之前调用该插件
  • 默认:在 Vite 核心插件之后调用该插件
  • post:在 Vite 构建插件之后调用该插件

一个 Vite 插件可以额外指定一个 enforce 属性(类似于 webpack 加载器)来调整它的应用顺序。enforce 的值可以是pre 或 post。解析后的插件将按照以下顺序排列:

  • Alias
  • 带有 enforce: ‘pre’ 的用户插件
  • Vite 核心插件
  • 没有 enforce 值的用户插件
  • Vite 构建用的插件
  • 带有 enforce: ‘post’ 的用户插件
  • Vite 后置构建插件(最小化,manifest,报告)

示例:

ts
import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' import legacy from '@vitejs/plugin-legacy' export default defineConfig({ plugins: [ { ...legacy({ targets: ['defaults', 'not IE 11'] }), enforce: 'pre' }, vue() ] })

3.4 按需加载插件

默认情况下插件在开发 (serve) 和生产 (build) 模式中都会调用。如果插件在服务或构建期间按需使用,请使用 apply 属性指明它们仅在 ‘build’ 或 ‘serve’ 模式时调用

示例:

ts
export default defineConfig({ plugins: [ { ...legacy({ targets: ['defaults', 'not IE 11'] }), enforce: 'pre', apply: 'build' }, ] })

3.5 自定义插件

配置约定

如果插件不使用 Vite 特有的钩子,可以作为 兼容 Rollup 的插件 来实现,推荐使用 Rollup 插件名称约定。

-Rollup 插件应该有一个带 rollup-plugin- 前缀、语义清晰的名称。

  • 在 package.json 中包含 rollup-plugin 和 vite-plugin 关键字。 这样,插件也可以用于纯 Rollup 或基于 WMR 的项目。

对于 Vite 专属的插件:

  • Vite 插件应该有一个带 vite-plugin- 前缀、语义清晰的名称。
  • 在 package.json 中包含 vite-plugin 关键字。
  • 在插件文档增加一部分关于为什么本插件是一个 Vite 专属插件的详细说明(如,本插件使用了 Vite 特有的插件钩子)。

如果你的插件只适用于特定的框架,它的名字应该遵循以下前缀格式:

  • vite-plugin-vue- 前缀作为 Vue 插件
  • vite-plugin-react- 前缀作为 React 插件
  • vite-plugin-svelte- 前缀作为 Svelte 插件

vite 添加使用自定义插件

  1. 创建一个目录 my-plugin 执行初始化
  2. 创建 index.ts 添加测试内容
ts
import type { ResolvedConfig } from "vite"; const examplePlugin = () => { let config: any; return { name: "read-config", configResolved(resolvedConfig: ResolvedConfig) { console.log("resolvedConfig=>", resolvedConfig); // 存储最终解析的配置 config = resolvedConfig; }, // 在其他钩子中使用存储的配置 transform(code: string, id: string) { // console.log(code,id) if (config.command === "serve") { // dev: 由开发服务器调用的插件 } else { // build: 由 Rollup 调用的插件 } }, }; }; export default examplePlugin;
  1. vite.config.ts 中进行配置使用
ts
import examplePlugin from "./src/my-plugin/index"; export default defineConfig({ plugins: [ vue(), exmaplePlugin() ] })
  1. 启动项目查看插件是否被使用

image.png

3.6 vite 和 rollup 钩子函数

字段说明所属
name插件名称vite 和 rollup 共享
handleHotUpdate执行自定义 HMR(模块热替换)更新处理vite 独享
config在解析 Vite 配置前调用。可以自定义配置,会与 vite 基础配置进行合并vite 独享
configResolved在解析 Vite 配置后调用。可以读取 vite 的配置,进行一些操作vite 独享
configureServer是用于配置开发服务器的钩子。最常见的用例是在内部 connect 应用程序中添加自定义中间件。vite 独享
transformIndexHtml转换 index.html 的专用钩子。vite 独享
options在收集 rollup 配置前,vite (本地)服务启动时调用,可以和 rollup 配置进行合并vite 和 rollup 共享
buildStart在 rollup 构建中,vite (本地)服务启动时调用,在这个函数中可以访问 rollup 的配置vite 和 rollup 共享
resolveId在解析模块时调用,可以返回一个特殊的 resolveId 来指定某个 import 语句加载特定的模块vite 和 rollup 共享
load在解析模块时调用,可以返回代码块来指定某个 import 语句加载特定的模块vite 和 rollup 共享
transform在解析模块时调用,将源代码进行转换,输出转换后的结果,类似于 webpack 的 loadervite 和 rollup 共享
buildEnd在 vite 本地服务关闭前,rollup 输出文件到目录前调用vite 和 rollup 共享
closeBundle在 vite 本地服务关闭前,rollup 输出文件到目录前调用vite 和 rollup 共享

4 实现原理

使用 Vite 构建前端项目时,Vite 会在一开始将项目中的代码分为依赖和源码两大类。

4.1 对依赖的处理

依赖即开发项目时使用的第三方库,一个稍大的项目会有成百上千个依赖,处理这些依赖库的性能代价是比较高的,而且不同依赖库使用的模块化规则不同(如 UMD、ESM 或 CommonJS),所以还需要将其转成相关的模块化规则。

对于依赖的处理,我们通常称为依赖预构建,Vite 使用 esbuild 来实现依赖预构建,将 CommonJS 和 UMD 的依赖库转为 ESM 形式,此外因为依赖库大多数时候不会变化,Vite 会将构建好的依赖存到 node_modules/.vite 目录中,如果依赖变化(package.json 等文件中依赖变化了)则会重新构建。

Vite 使用 esbuild 这个全新 js 打包工具的原因是该工具使用 Go 开发,相比于 gulp、rollup 等使用 JS 开发的传统打包工具,esbuild 会快上十几倍,因为打包其实是 CPU 密集型操作,编译型语言会比动态语言快很多(目前前端的一个趋势便是使用编译型语言开发的工具去替代已有的一些工具)。 esbuild 目前还比较新,虽然重要的功能都有了,但还是有部分功能缺失,如对 CSS 处理还不太好,所以 Vite 目前只是要 esbuild 实现开发期间的构建操作,项目开发完要正式构建时使用的是 rollup,rollup 非常成熟,虽然慢一点,但正式构建在开发过程中不会太频繁。

4.2 对源码的处理

源码,通常就是我们开发项目时自己写的 JS 代码,这些代码时常被修改,但并不是每次修改都需要重新加载所有源码的,Vite 利用浏览器 ES Modules(ESM),当浏览器发起相应模块的请求时,Vite 内置的基于 Koa 构建的 web 服务器会拦截 ES Module 请求,并通过 path 找到想要目录的文件,通过简单的处理再返回给浏览器。

Vite 跳过了打包的动作,Webpack 每次需要将代码打包成一个完整的 JS,当项目变大时,打包就会变慢,这是根本原因,而 Vite 在开发阶段构建项目时会将其构建成 ESM 的形式,这让浏览器来决定什么使用要加载什么模块,然后 Vite 拦截并处理浏览器对模型加载的请求,从而实现真正的按需加载,不再需要打包。

通过 Vite 官网中的两张图,可以更清晰的理解其中的差异:

image.png

image.png

5. vite 和 webpack 的区别

webpack是先打包再启动开发服务器,vite是直接启动开发服务器,然后按需编译依赖文件,vite基本原理是无需打包,依赖浏览器原生支持esm 去解析模块.

  • webpack先打包,再启动开发服务器,请求服务器时直接给予打包后的结果;
  • vite直接启动开发服务器,请求哪个模块再对哪个模块进行实时编译;
  • 热更新方式:
    • webpack 的热更新是基于 dev-servr 每次热更新都要重新打包。当我们开始构建越来越大型的应用时,需要处理的 JavaScript 代码量也呈指数级增长。
    • vite 热更新是会起2个服务:客户端服务,websocket服务,监听本地文件变更,区分变更的类型,然后做相应的处理,通过ws通知客户端服务,客户端服务在去加载新的资源或刷新网页,热更新时,不同的文件有不同的处理方式
      • 当你只修改了 script 里的内容时:不会刷新,
      • Vue 组件重新加载(会重新走生命周期),
      • 当你只修改了 template 里的内容时: 不会刷新,
      • Vue 组件重新渲染(不会重新走生命周期)
      • 样式更新,样式移除时:不会刷新,直接更新样式,覆盖原来的
      • js 文件更新时:网页重刷新
  • webpack由于现代浏览器本身就支持ES Modules,会主动发起请求去获取所需文件。vite充分利用这点,将开发环境下的模块文件,就作为浏览器要执行的文件,而不是像webpack先打包,交给浏览器执行的文件是打包后的;
  • 由于vite启动的时候不需要打包,也就无需分析模块依赖、编译,所以启动速度非常快。当浏览器请求需要的模块时,再对模块进行编译,这种按需动态编译的模式,极大缩短了编译时间,当项目越大,文件越多时,vite的开发时优势越明显;
  • 在HRM方面,当某个模块内容改变时,让浏览器去重新请求该模块即可,而不是像webpack重新将该模块的所有依赖重新编译;
  • 当需要打包到生产环境时,vite使用传统的rollup进行打包,所以,vite的优势是体现在开发阶段,另外,由于vite使用的是ES Module,所以代码中不可以使用CommonJs;
如果对你有用的话,可以打赏哦
打赏
ali pay
wechat pay

本文作者:叶继伟

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!