Vite学习笔记

128 阅读12分钟

为什么选择Vite

根据官方文档,此节从几个不同的角度进行思考。

现实问题

在浏览器支持ES模块之前,JavaScript并没有提供原生的机制让开发者以模块化的方式进行开发。也因此衍生出了关于“打包”的概念:使用工具抓取、处理,并将模块源码串联成可以在浏览器中运行的文件。

后续推出的WebPackRollupParcel等工具,虽然极大地改善了前端开发者的开发体验,但也仍然存在一些不足:当开始构建越来越大型的应用时,需要处理的JavaScript代码、模块也成指数级增长,这些开发工具有时需要耗时几分钟才可以启动开发服务器。即使使用模块热替换(HMR),文件修改后的效果也需要几秒钟才能在浏览器呈现出来,这也极大地影响了开发效率。

Vite的核心即利用生态系统中的新进展来解决上述问题:

  1. 浏览器开始原生支持ES模块
  2. 越来越多的JavaScript工具使用编译型语言编写

缓慢的服务器启动

在使用冷启动开发器的时候,基于打包器的方式启动必须优先抓取并构建整个应用,然后才能提供服务。

Vite的不同之处在于:Vite在一开始就将应用中的模块区分为依赖源码两类,改进了开发服务器的启动时间。

  • 依赖 大多为在开发时不会变动的纯JavaScript。一些较大的依赖,例如有上百个模块的组件库,其处理代价也很高。依赖通常也会存在多种模块化的格式,例如ESMCommonJS

    • Vite将会使用esbuild预构建依赖。esbuild使用Go语言编写,并且比以JavaScript编写的打包器速度快10-100倍。
  • 源码 通常包含一些并非直接是JavaScript的文件,需要转换(例如JSXCSS或者Vue/React组件),这些文件经常会被编辑修改。于此同时,并不是所有的源码都需要被重新加载,例如:基于路由拆分的代码模块,只需要在使用该模块时再进行加载即可。

    • Vite以原生ESM的方式提供源码。这实际上是让浏览器接管了打包程序的部分工作:

      • Vite只需要在浏览器请求源码时,才进行转换并按需提供源码
      • 根据情景动态导入代码,即只在当前屏幕上实际使用时才会被处理

常规的打包工具:

Vite:

缓慢的更新

基于打包器启动时,重建整个包的效率很低,这是因为:更新速度会随着应用体积的增长而直线下降

一些打包器的开发服务器将构建内容存入内存,这样它们只需要在文件更改时使模块图的一部分失活,但尽管如此,它也仍然需要重新构建整个页面,并重载页面。这样代价很高,会导致页面消除当前的应用状态。至此,打包器支持了动态模块热替换,可以确保页面不重载。然而,实践证明,即使采用了HMR模式,其热更新速度也会随着应用规模的增长而显著下降。

在Vite中,HMR是在原生ESM上执行的。当编辑一个文件的时候,Vite只需要精确地使已编辑的模块与其最近的HMR边界之间的失活(大多数情况只是模块本身),使得无论应用大小如何,HMR始终能保持快速更新。相比一般打包器的让依赖图中的一部分失活,Vite的只让相关的链失活可以提升开发效率。

除此之外,Vite同时利用HTTP头来加速整个页面的重新加载,可以利用浏览器来帮助做一些事:

  • 源码模块的请求会根据304 Not Modified进行协商缓存
  • 依赖模块的请求则通过Cache-Control:max-age = 3153600, immutable来进行强缓存。因此,一旦他被缓存,就不再需要再次请求。

为什么生产环境仍需打包

尽管现在原生ESM得到了广泛支持,但由于嵌套导入会导致额外的网络往返,在生产环境中发布未打包的ESM仍然效率低下(即使使用HTTP/2)。为了在生产环境中获得最佳的加载性能,最好还是将代码进行tree-shaking、懒加载和chunk分隔(以获得更好的缓存)。

总览

Vite是一种新型前端构建工具,能够显著提升前端开发体验。它主要由两部分组成:

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

浏览器支持

默认的构建目标是能支持原生ESM语法的script标签、原生ESM动态导入import.meta的浏览器。传统浏览器可以通过官方插件@vitejs/plugin-legacy支持。

除此之外,目前支持如下模版:

关于index.html

在一个Vite项目中,index.html在项目的最外层而不是在public文件夹内,这是有意而为之的:在开发期间,Vite可以被视作一个服务器,而index.html是该Vite项目的入口文件

Vite将index.html视为源码和模块图的一部分。Vite解析<script type="module" src="...">,这个标签指向项目的JavaScript源码。除此之外,还可以直接饮用CSS的<link href>

功能介绍

对于非常基础的使用来说,使用Vite开发和使用一个静态服务器并没有太大的区别。然而,Vite还通过原生ESM导入提供了许多主要用于打包场景的增强功能。

NPM依赖解析和预构建

原生ES导入不支持下面这样的裸模块导入:

import { someMethod } from 'my-dep'

上面的代码会在浏览器中抛出一个错误。Vite将会检测到所有被加载的源文件中的以上述方式导入的模块,并执行以下操作:

  1. 预构建它们可以提高页面加载速度,并将CommonJS/UMD转换为ESM格式。预构建这一步由ESBuild执行,这使得Vite的冷启动时间比任何基于JavaScript的打包器都要快得多。
  2. 重写导入为合法的URL,例如/node_modules/.vite.deps/my-dep.js?v=f3sf2ebd以便浏览器能够正确导入它们。

模块热替换

Vite提供了一套原生ESM的HMR API。具有HMR功能的框架可以直接利用该API提供及时、准确的更新,而无需重新加载页面或清除应用程序状态。Vite内置了HMR到Vue单文件组件(SFC)React Fast Refresh

TypeScript相关

Vite天然支持引入.ts文件,但是需要注意的是:Vite仅执行.ts文件的转译工作,不执行任何类型检查。

Vite的工作是尽可能快地将源模块转化为可以在浏览器中运行的形式。为此,应尽可能地将静态分析检查与Vite的转换管道分开。这一原则也适用于其他静态分析检查,例如ESLint

Vite使用ESBuild将TypeScript转译到JavaScript,速度约是tsc的20~30倍。

与此同时,Vite也支持处理.jsx.tsx.module.css文件。

静态资源处理

导入一个静态资源会返回解析后的URL:

    import imgUrl from './img.png'//会返回一个解析后的URL
    
    document.getElementById('img-container').src = imgUrl

除此之外,也可以通过添加一些特殊的查询参数来更改资源被引入的方式:

    //显式加载资源为一个URL
    import assetAsUrl from './asset.js?url'
    
    //以字符串的形式来加载资源
    import assetAsString from './shader.glsl?raw'
    
    //加载为Web Worker
    import Worker from './worker.js?worker'
    
    //在构建时 Web Worker内联为base64字符串
    import InlineWorker from './worker.js?worker&line'

JSON文件也可以被直接导入 ———— 同样支持具名导入:

    //导入整个对象
    import json from './example.json'
    
    //对一个根字段使用具名导入,可以有效帮助treeshaking
    import { field } from './example.json'

命令行界面

开发服务器

在当前目录下启动Vite开发服务器。

使用

vite [root]

选项

构建

构建生产版本

使用

vite build [root]

关于构建生产版本时的选项,参照官方文档:cn.vitejs.dev/guide/cli.h…

除此之外,还有一些可配置的命令:

  • vite optimize [root] 预构建依赖
  • vite preview [root] 本地预览构建产物

使用插件

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

添加一个插件

若要使用一个插件,则需要将它添加到项目的devDependencies并在vite.config.js配置文件中的plugins数组中引入它。例如,当想引入官方插件@vitejs/plugin-legacy时:

  1. $ npm add -D @vitejs/plugin-legacy
  2. 引入到vite配置文件的plugins数组中
    //vite.config.js
    import legacy from '@vitejs/plugin-legacy'
    import { defineConfig } from 'vite'
    
    export default defineConfig({
        plugins:[
            legacy({
                targets: ['defaults', 'not IE 11'],
            })
        ]
    })

强制插件排序

为了与某些Rollup插件兼容,可能需要强制修改插件的执行顺序,或者只在构建时使用。可以使用enforce修饰符来规定插件的位置:

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

例:

    //vite.config.js
    import image from '@rollup/plugin-image'
    import { defineConfig } from 'vite'
    
    export default defineConfig({
        plugins: [
            {
                ...image(),
                enforce: 'pre',
            }
        ]
    })

按需应用

默认情况下插件在开发(serve)和生产(build)模式中都会调用。如果想指定插件只在特定的期间使用,则需要使用apply关键字来指明,共有buildserve两个选项

    // vite.config.js
    import typescript2 from 'rollup-plugin-typescript2'
    import { defineConfig } from 'vite'

    export default defineConfig({
        plugins: [
            {
                ...typescript2(),
                apply:'build',
            }
        ]
    })

依赖预构建

当首次使用Vite来启动项目时,vite会预先构建整个项目将会遇到的依赖。

原因

提前预构建依赖主要有两个目的:

  1. CommonJSUMD兼容性: 在开发阶段中,Vite的开发服务器将所有代码视为原生ES模块。因此,Vite必须先将作为CommonJS或UMD发布的依赖项转换为ESM。

    当转换CommonJS依赖时,Vite会执行智能导入分析,这样即使导出是动态分配的(如React),按名导入也会符合预期效果:

       //符合预期
       import React, { useState } from 'react'
    
  2. 性能: Vite将有许多内部模块的ESM依赖关系转换为单个模块,以提高后续页面的加载性能。

    一些包将它们的ES模块构建作为许多单独的文件相互导入。例如,lodash-es有超过600个内置模块,而这600个模块存在相互导入的情况。所以,当执行import { debounce } from 'lodash-es'时,浏览器会同时发出600多个HTTP请求。大量的请求会在浏览器端造成网络拥塞,导致页面的加载速度变慢。

    此时使用Vite对loadsh-es进行预构建,将其构建成一个模块,这样就只需要发送一个http请求了。这也会相应地提升页面的渲染速度。

注意:

依赖预构建仅会在开发模式下应用,并会使用esbuild将依赖转换为ESM模块。而在生产构建中,需要使用@rollup/plugin-commonjs来进行预构建工作

自动依赖搜寻

如果没有找到相应的缓存,Vite将抓取项目中的源码,并自动寻找引入的依赖项(即"bare import",表示期望从node_modules解析),并将这些依赖项作为预构建包的入口点。预构建通过esbuild执行,所以它通常非常快。

在服务器已经启动之后,如果遇到一个新的依赖关系导入,而这个依赖还没有被预构建到缓存中,则Vite将重新运行依赖构建进程并重新加载页面。(只有在添加新依赖的时候,页面会重新加载,页面的应用状态会丢失)

Monorepo和链接依赖

在一个monorepo启动中,该仓库中的某个包可能会成为另一个包的依赖。Vite会自动侦测没有从node_modules解析的依赖项,并将依赖链接到的文件视作源码。Vite不会尝试打包这个源码,而是会分析这个被链接依赖的源码的依赖列表

然而,这需要被链接的依赖可以被导出为ESM格式。如果不是,则需要在配置里添加到optimizeDeps.includebuild.commonjsOptions.include中。

    export default defineConfig({
      optimizeDeps: {
        include: ['linked-dep'],
      },
      build: {
        commonjsOptions: {
          include: [/linked-dep/, /node_modules/],
        },
      },
    })
    
    //添加完毕后,添加命令 --force 并重启服务器

缓存

文件系统缓存

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

  • 包管理器的lockfile内容,例如package-lock.jsonyarn.lockpnpm-lock.yaml,或者bun.lockb
  • 补丁文件夹的修改时间
  • 可能在vite.config.js相关字段中配置过的
  • NODE_ENV的值

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

如果想要强制Vite重新构建依赖,则可以使用--force命令行启动开发服务器,或者手动删除node_modules/.vite目录。

浏览器缓存

解析后的依赖请求会以HTTP头max-age=31536000, immutable来进行强缓存,以提高在开发时的页面重载性能。一旦被缓存,浏览器将永远不再请求这些资源。可以提升渲染和开发效率。

如果安装了不同版本的依赖(这反映在包管理器的lockfile中),则附加的版本query会自动使它们失效。如果想要通过本地来调整依赖项,可以:

  • 在浏览器的Network选项卡中暂时禁用缓存;
  • 重启Vite dev server,并添加--force命令以重新构建依赖;
  • 重新载入页面;