Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

【基础】vite使用详解 #6

Open
wbccb opened this issue Mar 24, 2023 · 1 comment
Open

【基础】vite使用详解 #6

wbccb opened this issue Mar 24, 2023 · 1 comment
Labels
基础 待完善 初步版本,还需要补充细节

Comments

@wbccb
Copy link
Owner

wbccb commented Mar 24, 2023

vite相比较于webpack的优势

vite产生的背景

当我们开始构建越来越大型的应用时,需要处理的 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 只需要在浏览器请求源码时进行转换并按需提供源码。根据情景动态导入代码,即只在当前屏幕上实际使用时才会被处理。

缓慢的更新

部分模块更新时,热更新成功所需要的时间

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

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

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

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

vite的缺点

开发服务器和生产环境构建之间输出和行为可能不一致

生产环境仍然需要打包

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

生产环境不使用ESBuild打包

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

vite的组成

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

开箱即用意味着很多类型文件的解析不需要像webpack.config.js那样进行配置,比如解析node_modules对应的模块时会自动补全路径

vite的初始化

使用 NPM:

$ npm create vite@latest

使用 Yarn:

$ yarn create vite

使用 PNPM:

$ pnpm create vite

vite的功能

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

可以理解为Vite起了一个本地服务器,然后又添加了很多增加功能用于打包,下面将描述提供的增加功能

NPM依赖解析和预构建

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

import { someMethod } from 'my-dep'

但是Vite会检测到这些裸模块的导入,并执行下面的操作:

  1. 预构建 它们可以提高页面加载速度,并将 CommonJS / UMD 转换为 ESM 格式。

预构建这一步由 esbuild 执行,这使得 Vite 的冷启动时间比任何基于 JavaScript 的打包器都要快得多。

  1. 重写导入为合法的 URL,将node_modules预构建(模块中多个import预构建成为单个文件,减少网络加载的文件数量)的模块放入缓存中,以便浏览器能够正确导入它们。并且依赖是强缓存的。

例如 /node_modules/.vite/deps/my-dep.js?v=f3sf2ebd

预构建缓存

.vite

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

包管理器的 lockfile 内容,例如 package-lock.json,yarn.lock,pnpm-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 命令以重新构建依赖;
重新载入页面。

模块热更新

Vite 提供了一套原生 ESM 的 HMR API。 具有 HMR 功能的框架可以利用该 API 提供即时、准确的更新,而无需重新加载页面或清除应用程序状态。Vite 内置了 HMR 到 Vue 单文件组件(SFC) 和 React Fast Refresh 中。也通过 @prefresh/vite 对 Preact 实现了官方集成。

注意,你不需要手动设置这些 —— 当你通过 create-vite 创建应用程序时,所选模板已经为你预先配置了这些。

TypeScript

Vite 天然支持引入 .ts 文件。

不进行类型检测!

Vite 仅执行 .ts 文件的转译工作,并不执行 任何类型检查。并假定类型检查已经被你的 IDE 或构建过程处理了。

Vite 之所以不把类型检查作为转换过程的一部分,是因为这两项工作在本质上是不同的。转译可以在每个文件的基础上进行,与 Vite 的按需编译模式完全吻合。相比之下,类型检查需要了解整个模块图。把类型检查塞进 Vite 的转换管道,将不可避免地损害 Vite 的速度优势。

Vite 的工作是尽可能快地将源模块转化为可以在浏览器中运行的形式。为此,我们建议将静态分析检查与 Vite 的转换管道分开。

你可以在构建生产版本时,你可以在 Vite 的构建命令之外运行tsc --noEmit进行类型检查

在开发时,如果你需要更多的 IDE 提示,我们建议在一个单独的进程中运行tsc --noEmit --watch,或者如果你喜欢在浏览器中直接看到上报的类型错误,可以使用vite-plugin-checker

使用esbuild转译

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

转译时的注意事项

tsconfig.jsoncompilerOptions 下的一些配置项需要特别注意。

  • isolatedModules: true

这是因为 esbuild 只执行没有类型信息的转译,它并不支持某些特性,如 const enum 和隐式类型导入。

你必须在 tsconfig.json 中的 compilerOptions 下设置 "isolatedModules": true。如此做,TS 会警告你不要使用隔离(isolated)转译的功能。

然而,一些库(如:vue)不能很好地与 "isolatedModules": true 共同工作。你可以在上游仓库修复好之前暂时使用 "skipLibCheck": true 来缓解这个错误。

如果你的代码库很难迁移到 "isolatedModules": true,或许你可以尝试通过第三方插件来解决,比如 rollup-plugin-friendly-type-imports。但是,这种方式不被 Vite 官方支持。

  • useDefineForClassFields: true

大多数库都希望 "useDefineForClassFields": true,如 MobX,Vue Class Components 8.x 等。

但是有几个库还没有兼容这个新的默认值,其中包括 lit-element。如果遇到这种情况,请将 useDefineForClassFields 设置为 false。

Vue

  • Vue 3 单文件组件支持:@vitejs/plugin-vue
  • Vue 3 JSX 支持:@vitejs/plugin-vue-jsx
  • Vue 2.7 支持:vitejs/vite-plugin-vue2
  • Vue <2.7 的支持:underfin/vite-plugin-vue2

JSX

.jsx.tsx 文件同样开箱即用。JSX 的转译同样是通过 esbuild。

Vue 用户应使用官方提供的 @vitejs/plugin-vue-jsx 插件,它提供了 Vue 3 特性的支持,包括 HMR,全局组件解析,指令和插槽。

你可以使用 jsxInject(这是一个仅在 Vite 中使用的选项)为 JSX 注入 helper,以避免手动导入:

// vite.config.js
import { defineConfig } from 'vite'

export default defineConfig({
  esbuild: {
    jsxInject: `import React from 'react'`,
  },
})

CSS

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

Vite 通过 postcss-import 预配置支持了 CSS @import 内联,Vite 的路径别名也遵从 CSS @import

Sass 和 Less 文件也支持 @import 别名和 URL 自动重新设置基准以确保正确性

CSS预处理器

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

# .scss and .sass
npm add -D sass

# .less
npm add -D less

# .styl and .stylus
npm add -D stylus

PostCSS

PostCSS不是一个预处理器(尽管它的行为很像)。 它是一个Node.js工具,它采用有效的CSS并对其进行增强。即使是那些使用Sass、Less或Stylus的人,也经常在初始CSS编译后运行PostCSS步骤。你可能遇到过PostCSS的Autoprefixer插件 ,它可以在需要前缀的CSS属性中自动添加 -webkit, -moz, and -ms 前缀。

就其本身而言,PostCSS什么也不做。它是一个对CSS代码进行标记以创建抽象语法树的解析器。一个插件可以处理这个树并相应地更新属性。一旦所有插件完成了它们的工作,PostCSS将所有内容重新格式化为字符串并输出到CSS文件中。

大约有 350 个插件可用,大多数都是执行单一的任务,如 代码嵌入 @import 声明,简化 calc() 函数, 处理图片资源, 语法提示, 压缩等等。 在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 的行为。选项将被传递给 postcss-modules。

interface CSSModulesOptions {
  scopeBehaviour?: 'global' | 'local'
  globalModulePaths?: RegExp[]
  generateScopedName?:
    | string
    | ((name: string, filename: string, css: string) => string)
  hashPrefix?: string
  /**
   * 默认:null
   */
  localsConvention?:
    | 'camelCase'
    | 'camelCaseOnly'
    | 'dashes'
    | 'dashesOnly'
    | null
}

禁用CSS注入页面

import './foo.css' // 样式将会注入页面
import otherStyles from './bar.css?inline' // 样式不会注入页面

静态资源处理

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

import imgUrl from './img.png'
document.getElementById('hero-img').src = imgUrl

添加一些特殊的查询参数可以更改资源被引入的方式:

// 以字符串形式加载资源
import assetAsString from './shader.glsl?raw'

JSON

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

// 导入整个对象
import json from './example.json'
// 对一个根字段使用具名导入 —— 有效帮助 treeshaking!
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。如果你倾向于直接引入所有的模块(例如依赖于这些模块中的副作用首先被应用),你可以传入 { eager: true } 作为第二个参数:

const modules = import.meta.glob('./dir/*.js', { eager: true })

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

// 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,
}

默认是模块形式导入,也支持字符串形式导入资源/支持将资源作为URL加载

const modules = import.meta.glob('./dir/*.js', { as: 'raw' })
const modules = import.meta.glob('./dir/*.js', { as: 'url' })

也支持多个匹配模式、反面匹配模式、自定义查询等形式导入,这是Vite提供的独特功能

所有 import.meta.glob 的参数都必须以字面量传入。你 不 可以在其中使用变量或表达式。

该 Glob 模式会被当成导入标识符:必须是相对路径(以 ./ 开头)或绝对路径(以 / 开头,相对于项目根目录解析)或一个别名路径(请看 resolve.alias 选项)。

内置的构建优化

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

CSS代码分割

Vite 会自动地将一个异步 chunk 模块中使用到的 CSS 代码抽取出来并为其生成一个单独的文件。这个 CSS 文件将在该异步 chunk 加载完成时自动通过一个 标签载入,该异步 chunk 会保证只在 CSS 加载完毕后再执行。

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

预加载指令生成

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

异步Chunk加载优化

Entry ---> A ---> C

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

Entry ---> (A + C)

插件

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

添加插件

若要使用一个插件,需要将它添加到项目的 devDependencies 并在 vite.config.js 配置文件中的 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 插件兼容,可能需要强制修改插件的执行顺序,或者只在构建时使用。这应该是 Vite 插件的实现细节。可以使用 enforce 修饰符来强制插件的位置:

  • pre:在 Vite 核心插件之前调用该插件
  • 默认:在 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 属性指明它们仅在 'build' 或 'serve' 模式时调用:

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

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

其它

public 目录

如果你有下列这些资源:

  • 不会被源码引用(例如 robots.txt)
  • 必须保持原有文件名(没有经过 hash)
  • ...或者你压根不想引入该资源,只是想得到其 URL。
    那么你可以将该资源放在指定的 public 目录中,它应位于你的项目根目录。该目录中的资源在开发时能直接通过 / 根路径访问到,并且打包时会被完整复制到目标目录的根目录下。

目录默认是 <root>/public,但可以通过 publicDir 选项 来配置。

请注意:

  • 引入 public 中的资源永远应该使用根绝对路径 —— 举个例子,public/icon.png 应该在源码中被引用为 /icon.png
  • public 中的资源不应该被 JavaScript 文件引用。

自定义构建

构建过程可以通过多种 构建配置选项 来自定义构建。具体来说,你可以通过 build.rollupOptions 直接调整底层的 Rollup 选项:

// vite.config.js
export default defineConfig({
  build: {
    rollupOptions: {
      // https://rollupjs.org/configuration-options/
    },
  },
})

产物分块策略

你可以通过配置 build.rollupOptions.output.manualChunks 来自定义 chunk 分割策略(查看 Rollup 相应文档)。在 Vite 2.8 及更早版本中,默认的策略是将 chunk 分割为 index 和 vendor。这对一些 SPA 来说是好的策略,但是要对所有应用场景提供一种通用解决方案是非常困难的。从 Vite 2.9 起,manualChunks 默认情况下不再被更改。你可以通过在配置文件中添加 splitVendorChunkPlugin 来继续使用 “分割 Vendor Chunk” 策略:

// vite.config.js
import { splitVendorChunkPlugin } from 'vite'
export default defineConfig({
  plugins: [splitVendorChunkPlugin()],
})

环境变量

Vite 在一个特殊的 import.meta.env 对象上暴露环境变量。

Vite 使用 dotenv 从你的 环境目录 中的下列文件加载额外的环境变量:

.env                # 所有情况下都会加载
.env.local          # 所有情况下都会加载,但会被 git 忽略
.env.[mode]         # 只在指定模式下加载
.env.[mode].local   # 只在指定模式下加载,但会被 git 忽略

为了防止意外地将一些环境变量泄漏到客户端,只有以 VITE_ 为前缀的变量才会暴露给经过 vite 处理的代码。例如下面这些环境变量:

VITE_SOME_KEY=123
DB_PASSWORD=foobar

console.log(import.meta.env.VITE_SOME_KEY) // 123
console.log(import.meta.env.DB_PASSWORD) // undefined

vite开发环境与生产环境之间的区别

//TODO

vite SSR

// TODO

参考

  1. https://vitejs.dev/guide/
  2. https://juejin.cn/post/7064853960636989454#heading-24
@wbccb wbccb added the 基础 label Mar 24, 2023
@wbccb wbccb added the 待完善 初步版本,还需要补充细节 label Apr 7, 2023
@wbccb
Copy link
Owner Author

wbccb commented Apr 16, 2023

Vite插件顺序

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

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

Vite插件与Rollup插件的区别

Rollup插件钩子

在开发中,Vite 开发服务器会创建一个插件容器来调用 Rollup 构建钩子,与 Rollup 如出一辙。

以下钩子在服务器启动时被调用:

以下钩子会在每个传入模块请求时被调用:

以下钩子在服务器关闭时被调用:

Vite特有钩子(待优化)

  • config: 可用于修改 vite config,用户可以通过这个 hook 修改 config;例如 vite-aliases 这个插件可以帮助我们自动生成别名
  • configResolved: 在解析 Vite 配置后调用,用于获取解析完毕的 config,在这个 hook 中不建议修改 config。
  • configureServer: 用于给 dev server 添加自定义 middleware;例如 vite-plugin-mock 插件就是在这个生命周期调用的
  • configurePreviewServer:与 configureServer 相同但是作为预览服务器。vite preview插件就是利用这个钩子。
  • transformIndexHtml:注入变量,用来转换 HTML 的内容。vite-plugin-html 插件可以帮助我们在html里注入变量,就是利用这个钩子
  • handleHotUpdate:执行自定义 HMR 更新处理

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
基础 待完善 初步版本,还需要补充细节
Projects
None yet
Development

No branches or pull requests

1 participant