Vite(一)为什么选 Vite、开始、功能(NPM依赖解析和预构建、HMR、CSS、构建优化)

Vite(一)为什么选 Vite、开始、功能(NPM依赖解析和预构建、HMR、CSS、构建优化)

  • 网址:https://vitejs.cn/guide/why.html

1. 为什么选 Vite

现实问题

在浏览器支持 ES 模块之前,开发者没有以模块化的方式开发 JavaScript 的原生机制。这也是 “打包” 这个概念出现的原因:使用工具抓取、处理和链接我们的源码模块到文件中,使其可以运行在浏览器中。

时过境迁,我们见证了许多诸如 webpackRollupParcel 等工具的诞生,这些工具极大地改善了前端开发者的开发体验。

而,当我们开始构建越来越大型的应用时,需要处理的 JavaScript 代码量也呈指数级增长。大型项目包含数千个模块的情况并不少见。我们开始遇到性能瓶颈 —— 使用 JavaScript 开发的工具通常需要很长时间(甚至是几分钟!)才能启动开发服务器,即使使用 HMR,文件修改后的效果也需要几秒钟才能在浏览器中反映出来。如此循环往复,迟钝的反馈会极大地影响开发者的开发效率和幸福感

Vite 旨在利用生态系统中的新进展解决上述问题:浏览器开始原生支持 ES 模块,越来越多 JavaScript 工具使用编译型语言编写

缓慢的服务器启动

当冷启动开发服务器时,基于打包器的方式是在提供服务前去急切地抓取和构建你的整个应用

Vite 通过在一开始将应用中的模块区分为 依赖源码 两类,改进了开发服务器启动时间

  • 依赖 大多为纯 JavaScript 并在开发时不会变动。一些较大的依赖(例如有上百个模块的组件库)处理的代价也很高。依赖也通常会以某些方式(例如 ESM 或者 CommonJS)被拆分到大量小模块中。

    Vite 将会使用 esbuild 预构建依赖。Esbuild 使用 Go 编写,并且比以 JavaScript 编写的打包器预构建依赖快 10-100 倍

  • 源码 通常包含一些并非直接是 JavaScript 的文件,需要转换(例如 JSX,CSS 或者 Vue/Svelte 组件),时常会被编辑。同时,并不是所有的源码都需要同时被加载。(例如基于路由拆分的代码模块)。

    Vite 以 原生 ESM 方式服务源码。这实际上是让浏览器接管了打包程序的部分工作:Vite 只需要在浏览器请求源码时进行转换并按需提供源码。根据情景动态导入的代码,即只在当前屏幕上实际使用时才会被处理

    基于打包器的开发服务器

    基于 ESM 的开发服务器

缓慢的更新

当基于打包器启动时,编辑文件后将重新构建文件本身。显然我们不应该重新构建整个包,因为这样更新速度会随着应用体积增长而直线下降

一些打包器的开发服务器将构建内容存入内存,这样它们只需要在文件更改时使模块图的一部分失活[1],但它也仍需要整个重新构建并重载页面。这样代价很高,并且重新加载页面会消除应用的当前状态,所以打包器支持了动态模块热重载(HMR):允许一个模块 “热替换” 它自己,而对页面其余部分没有影响。这大大改进了开发体验 - 然而,在实践中我们发现,即使是 HMR 更新速度也会随着应用规模的增长而显著下降

在 Vite 中,HMR 是在原生 ESM 上执行的。当编辑一个文件时,Vite 只需要精确地使已编辑的模块与其最近的 HMR 边界之间的链失效(大多数时候只需要模块本身),使 HMR 更新始终快速,无论应用的大小

Vite 同时利用 HTTP 头来加速整个页面的重新加载(再次让浏览器为我们做更多事情):源码模块的请求会根据 304 Not Modified 进行协商缓存,而依赖模块请求则会通过 Cache-Control: max-age=31536000,immutable 进行强缓存,因此一旦被缓存它们将不需要再次请求

一旦你体验到 Vite 有多快,我们十分怀疑你是否愿意再忍受像曾经那样使用打包器开发。

为什么生产环境仍需打包

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

要确保开发服务器和产品构建之间的最佳输出和行为一致并不容易。所以 Vite 附带了一套 预配置、预优化构建命令,开箱即用。

为何不用 ESBuild 打包?

虽然 esbuild 快得惊人,并且已经是一个在构建库方面比较出色的工具,但一些针对构建 应用 的重要功能仍然还在持续开发中 —— 特别是代码分割和 CSS 处理方面。就目前来说,Rollup 在应用打包方面更加成熟和灵活。尽管如此,当未来这些功能稳定后,我们也不排除使用 esbuild 作为生产构建器的可能。

Vite 与 X 的区别是?

你可以查看 比较 章节获取更多细节,了解 Vite 与同类工具的异同。

译者注[1] 暂以意译方式呈现。

2. 开始

总览

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

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

你可以在 为什么选 Vite 中了解更多关于项目的设计初衷。

浏览器支持

搭建第一个 Vite 项目

兼容性注意

Vite 需要 Node.js 版本 >= 12.0.0。

使用 NPM:

npm init @vitejs/app

使用 Yarn:

yarn create @vitejs/app

然后按照提示操作即可!

你还可以通过附加的命令行选项直接指定项目名称和你想要使用的模板。例如,要构建一个 Vite + Vue 项目,运行:

# npm 6.x
npm init @vitejs/app my-vue-app --template vue

# npm 7+, 需要额外的双横线:
npm init @vitejs/app my-vue-app -- --template vue

# yarn
yarn create @vitejs/app my-vue-app --template vue

支持的模板预设包括

  • vanilla
  • vue
  • vue-ts
  • react
  • react-ts
  • preact
  • preact-ts
  • lit-element
  • lit-element-ts

查看 @vitejs/create-app 获取每个模板的更多细节。

社区模板

@vitejs/create-app 是一个快速生成主流框架基础模板的工具。查看 Awesome Vite 仓库的 社区维护模板,里面包含不同框架的模板。你可以用如 degit 之类的工具,使用社区模版来搭建项目。

npx degit user/project my-project
cd my-project

npm install
npm run dev

如果该项目使用 main 作为默认分支, 需要在项目名后添加 #main

npx degit user/project#main my-project

index.html 与项目根目录

你可能已经注意到,在一个 Vite 项目中,index.html 在项目最外层而不是在 public 文件夹内。这是有意而为之的:在开发期间 Vite 是一个服务器,而 index.html 是该 Vite 项目的入口文件

Vite 将 index.html 视为源码和模块图的一部分。Vite 解析 <script type="module" src="..."> ,这个标签指向你的 JavaScript 源码。甚至内联引入 JavaScript 的 <script type="module" src="..."> 和引用 CSS 的 <link href> 也能利用 Vite 特有的功能被解析。另外,index.html 中的 URL 将被自动转换,因此不再需要 %PUBLIC_URL% 占位符了。

与静态 HTTP 服务器类似,Vite 也有 “根目录” 的概念,即文件被提供的位置。你会看到它在整个文档中用 <root> 表示。源码中的绝对 URL 路径将以项目的 “根” 作为基础来解析,因此你可以像在普通的静态文件服务器上一样编写代码(并且功能更强大!)。Vite 还能够处理依赖关系,解析处于根目录外的文件位置,这使得它即使在基于 monorepo 的方案中也十分有用

Vite 也支持多个 .html 作入口点的 多页面应用模式

指定替代根目录

vite 以当前工作目录作为根目录启动开发服务器。你也可以通过 vite serve some/sub/dir 来指定一个替代的根目录。

命令行界面

在安装了 Vite 的项目中,可以在 npm scripts 中使用 vite 可执行文件,或者直接使用 npx vite 运行它。下面是通过脚手架创建的 Vite 项目中默认的 npm scripts

{
  "scripts": {
    "dev": "vite", // 启动开发服务器
    "build": "vite build", // 为生产环境构建产物
    "serve": "vite preview" // 本地预览生产构建产物
  }
}

可以指定额外的命令行选项,如 --port--https。运行 npx vite --help 获得完整的命令行选项列表。

使用未发布的功能

如果你迫不及待想要体验最新的功能,可以自行克隆 vite 仓库 到本地机器上然后自行将其链接(将需要 Yarn 1.x):

git clone https://github.com/vitejs/vite.git
cd vite
yarn
cd packages/vite
yarn build
yarn link

然后,回到你的 vite 项目并运行 yarn link vite。重新启动开发服务器(yarn dev)来体验新功能吧!

社区

如果你有疑问或者需要帮助,可以到 DiscordGitHub Discussions 社区来寻求帮助。

3. 功能

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

NPM 依赖解析和预构建

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

import { someMethod } from 'my-dep'

上面的操作将在浏览器中抛出一个错误。Vite 将在服务的所有源文件中检测此类裸模块导入,并执行以下操作:

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

依赖是强缓存的

Vite 通过 HTTP 头来缓存请求得到的依赖,所以如果你想要 编辑/调试 一个依赖,请跟随 这里 的步骤。

模块热重载

Vite 提供了一套原生 ESM 的 HMR API。 具有 HMR 功能的框架可以利用该 API 提供即时、准确的更新,而无需重新加载页面或删除应用程序状态。Vite 提供了第一优先级的 HMR 集成给 Vue 单文件组件(SFC)React Fast Refresh。也有对 Preact 的集成 @prefresh/vite

注意,你不需要手动设置这些 —— 当你 create an app via @vitejs/create-app 创建应用程序时,所选模板已经为你预先配置了这些

TypeScript

Vite 支持开箱即用地引入 .ts 文件

Vite 仅执行 .ts 文件的翻译工作,并 执行任何类型检查。并假设类型检查已经被你的 IDE 或构建过程接管了。(你可以在构建脚本中运行 tsc --noEmit 或者安装 vue-tsc 然后运行 vue-tsc --noEmit 来对你的 *.vue 文件做类型检查)。

Vite 使用 esbuild 将 TypeScript 翻译到 JavaScript,约是 tsc 速度的 20~30 倍,同时 HMR 更新反映到浏览器的时间小于 50ms

注意因为 esbuild 只执行转译工作而不含类型信息,所以它无需支持 TypeScript 的特定功能例如常量枚举和隐式 “type-only” 导入。你必须在你的 tsconfig.json 中的 compilerOptions 里设置 "isolatedModules": true,这样 TS 才会警告你哪些功能无法与独立编译模式一同工作

Client Types

Vite 默认的类型定义是写给它的 Node.js API 的。要将其补充到一个 Vite 应用的客户端代码环境中,请将 vite/client 添加到 tsconfig 中的 compilerOptions.types 下:

{
  "compilerOptions": {
    "types": ["vite/client"]
  }
}

这将会提供以下类型定义补充:

  • 资源导入 (例如:导入一个 .svg 文件)
  • import.meta.env 上 Vite 注入的在 的环境变量的类型定义
  • import.meta.hot 上的 HMR API 类型定义

Vue

Vite 为 Vue 提供第一优先级支持

JSX

.jsx.tsx 文件同样开箱即用。JSX 的翻译同样是通过 ESBuild,默认为 React 16 形式,React 17 形式的 JSX 在 esbuild 中的支持请看 这里.

如果不是在 React 中使用 JSX,自定义的 jsxFactoryjsxFragment 可以使用 esbuild 选项 进行配置。例如对 Preact:

// vite.config.js
export default {
  esbuild: {
    jsxFactory: 'h',
    jsxFragment: 'Fragment'
  }
}

更多细节详见 ESBuild 文档.

自定义插件还可以自动将 import React from 'react' 这类代码注入到每个文件中,以避免手工导入它们。请参阅 插件 API 了解如何编写这样的插件。

CSS

导入 .css 文件将会把内容插入到 <style> 标签中,同时也带有 HMR 支持。也能够以字符串的形式检索处理后的、作为其模块默认导出的 CSS

@import 内联和变基

Vite 通过 postcss-import 预配置支持了 CSS @import 内联,Vite 的路径别名也遵从 CSS @import。换句话说,所有 CSS url() 引用,即使导入的文件在不同的目录中,也总是自动变基,以确保正确性。

PostCSS

如果项目包含有效的 PostCSS 配置 (任何受 postcss-load-config 支持的格式,例如 postcss.config.js),它将会自动应用于所有已导入的 CSS。

CSS Modules

任何以 .module.css 为后缀名的 CSS 文件都被认为是一个 CSS modules 文件。导入这样的文件会返回一个相应的模块对象:

/* example.module.css */
.red {
  color: red;
}
import classes from './example.module.css'
document.getElementById('foo').className = classes.red

CSS modules 行为可以通过 css.modules 选项 进行配置。

如果 css.modules.localsConvention 设置开启了 camelCase 格式变量名转换(例如 localsConvention: 'camelCaseOnly'), 你还可以使用按名导入。

// .apply-color -> applyColor
import { applyColor } from './example.module.css'
document.getElementById('foo').className = applyColor

请注意 CSS modules localsConvention 默认是 camelCaseOnly - 例如一个名为 .foo-bar 的类会被暴露为 classes.fooBar。CSS modules 行为可以通过 css.modules option 选项配置。

CSS 预处理器

因为 Vite 只针对现代浏览器,所以建议使用原生 CSS 变量和实现 CSSWG 草案的 PostCSS 插件(例如 postcss-nesting),并编写简单的、未来标准兼容的 CSS

话虽如此,但 Vite 也同时提供了对 .scss, .sass, .less, .styl.stylus 文件的内置支持。没有必要为他们安装特定的 vite 插件,但相应的预处理器依赖本身必须安装:

# .scss and .sass
npm install -D sass

# .less
npm install -D less

# .styl and .stylus
npm install -D stylus

如果是用的是单文件组件,可以通过 <style lang="sass">(或其他与处理器)自动开启。

Vite 为 Sass 和 Less 改进了 @import 解析,因而 Vite 别名也同样受用,另外,url() 中的相对路径引用的,与根文件不同目录中的 Sass/Less 文件会自动变基以保证正确性。

由于与其 API 冲突,@import 别名和 URL 变基不支持 Stylus。

你还可以通过在文件扩展名前加上 .module 来结合使用 CSS modules 和预处理器,例如 style.module.scss

静态资源处理

URL 导入

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

import imgUrl from './img.png'
document.getElementById('hero-img').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&inline'

更多细节请见 静态资源处理

JSON

JSON 可以被直接导入 - 同样支持具名导入:

// 导入整个对象
import json from './example.json'
// 对一个根字段使用具名导入 - 有效运用 tree-shaking!
import { field } from './example.json'

Glob 导入

Vite 支持使用特殊的 import.meta.glob 函数从文件系统导入多个模块:

const modules = import.meta.glob('./dir/*.js')

以上将会被转译为下面的样子:

// vite 生成的代码
const modules = {
  './dir/foo.js': () => import('./dir/foo.js'),
  './dir/bar.js': () => import('./dir/bar.js')
}

你可以遍历 modules 对象的 key 值来访问相应的模块:

for (const path in modules) {
  modules[path]().then((mod) => {
    console.log(path, mod)
  })
}

匹配到的文件将通过动态导入默认懒加载,并会在构建时分离为独立的 chunk。如果你倾向于直接引入所有的模块(例如依赖于这些模块中的副作用首先被应用),你可以使用 import.meta.globEager 代替:

const modules = import.meta.globEager('./dir/*.js')

以上会被转译为下面的样子:

// vite 生成的代码
import * as __glob__0_0 from './dir/foo.js'
import * as __glob__0_1 from './dir/bar.js'
const modules = {
  './dir/foo.js': __glob__0_0,
  './dir/bar.js': __glob__0_1
}

请注意:

  • 这只是一个 Vite 独有的功能而不是一个 Web 或 ES 标准
  • 该 Glob 模式会被当成导入标识符:必须是相对路径(以 ./ 开头,相对于项目根目录解析),从依赖中来的 Glob 不被支持。
  • Glob 导入只能使用默认导入(无法使用按名导入,也无法使用 import * as ...)。

Web Assembly

预编译的 .wasm 文件可以直接被导入 —— 默认导出将会是一个函数,返回值为所导出 wasm 实例对象的 Promise:

import init from './example.wasm'

init().then((exports) => {
  exports.test()
})

这个 init 函数也可以使用将传递给 WebAssembly.instantiate ,作为其第二个参数的 imports 对象:

init({
  imports: {
    someFunc: () => {
      /* ... */
    }
  }
}).then(() => {
  /* ... */
})

在生产构建当中,体积小于 assetInlineLimit.wasm 文件将会被内联为 base64 字符串。否则,它们将作为资源复制到 dist 目录中,并按需获取。

Web Worker

一个 web worker 脚本可以直接通过添加一个 ?worker 查询参数来导入。默认导出将是一个自定义的 worker 构造器:

import MyWorker from './worker?worker'

const worker = new MyWorker()

worker 脚本也可以使用 import 语句来替代 importScripts() - 注意,在开发过程中,这依赖于浏览器原生支持,目前只在 Chrome 中适用,而在生产版本中,它已经被编译掉了。

默认情况下,worker 脚本将在生产构建中作为单独的块发出。如果你想将 worker 内联为 base64 字符串,请添加 inline 查询参数:

import MyWorker from './worker?worker&inline'

构建优化

下面所罗列的功能会自动应用为构建过程的一部分,没有必要在配置中显式地声明,除非你想禁用它们。

动态导入 Polyfill

Vite 使用 ES 动态导入作为代码分割的断点。生成的代码也会使用动态导入来加载异步 chunk。然而浏览器对原生 ESM 动态导入的功能落地比对 type="module" script 块支持要晚,它们两个功能之间存在着浏览器兼容性差异。Vite 自动会生成一个轻量级的 动态导入 polyfill 来抹平二者差异。

如果你确定你的构建目标只有支持原生动态导入的浏览器,你可以通过 build.polyfillDynamicImport 显式地禁用这个功能。

CSS 代码分割

Vite 会自动地将一个异步 chunk 模块中使用到的 CSS 代码抽取出来并为其生成一个单独的文件。这个 CSS 文件将在该异步 chunk 加载完成时自动通过一个 <link> 标签载入,该异步 chunk 会保证只在 CSS 加载完毕后再执行,避免发生 [FOUC](https://en.wikipedia.org/wiki/Flash_of_unstyled_content#:~:text=A flash of unstyled content,before all information is retrieved.) 。

如果你更倾向于将所有的 CSS 抽取到一个文件中,你可以通过设置 build.cssCodeSplitfalse 来禁用 CSS 代码分割。

预加载指令生成

Vite 会为入口 chunk 和它们在打包出的 HTML 中的直接引入自动生成 <link rel="modulepreload"> 指令。

异步 Chunk 加载优化

在实际项目中,Rollup 通常会生成 “共用” chunk —— 被两个或以上的其他 chunk 共享的 chunk。与动态导入相结合,会很容易出现下面这种场景:

graph

在无优化的情境下,当异步 chunk A 被导入时,浏览器将必须请求和解析 A,然后它才能弄清楚它首先需要那个共用 chunk C。这会导致额外的网络往返:

Entry ---> A ---> C

Vite 将使用一个预加载步骤自动重写代码,来分割动态导入调用,因而当 A 被请求时,C 也将 同时 被获取到:

Entry ---> (A + C)

C 也可能有更深的导入,在未优化的场景中,这甚至会导致额外网络往返。Vite 的优化将跟踪所有的直接导入,无论导入深度如何,都完全消除不必要的往返。


版权声明:本文为qq_43456781原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。