Vite分享

380 阅读6分钟

vite是什么

Vite 是一个由原生ESM 驱动的 Web 开发构建工具。在开发环境下基于浏览器原生ES imports 开发,在生产环境下基于Rollup打包。

它主要具有以下特点:

  • 快速的冷启动
  • 即时的模块热更新
  • 真正的按需编译

Vite vs Webpack

在浏览器支持ESM之前,JavaScript 并没有提供原生机制让开发者以模块化的方式进行开发。这也正是我们对 “打包” 这个概念熟悉的原因:使用工具抓取、处理并将我们的源码模块串联成可以在浏览器中运行的文件。然而,当我们开始构建越来越大型的应用时,传统打包工具需要处理的 JavaScript 代码量也呈指数级增长。我们开始遇到性能瓶颈 —— 使用 JavaScript 开发的工具通常需要很长时间(甚至是几分钟!)才能启动开发服务器,即使使用 HMR,文件修改后的效果也需要几秒钟才能在浏览器中反映出来。而Vite 旨在利用生态系统中的新进展解决上述问题。

基于开发环境的数据

冷启动时间二次启动时间HMR时间
Vite4207ms833ms95ms
Webpack32960ms29466ms5535ms

开发环境编译模式

传统的bundle模式,以Webpack为例:

Bundle based dev server 从上图我们可以看出webpack打包分为以下步骤:

  1. 查找入口文件:从webpack的配置文件中查找entry的配置,从而找到入口文件。

  2. 分析依赖关系:接到入口文件之后,从入口文件出发,分析入口文件中依赖了哪些文件,并且这些依赖的文件中还可能依赖别的文件,就这么递归的找下去。

  3. 输出bundle:找到依赖中的所有文件,把这些文件转化成模块的函数,生成一个一个浏览器可执行的bundle。

  4. 启动服务:node创建本地服务器并启动静态页面。 从整个流程来看,它的问题是:

  5. dev serve必须等到所有的模块打包成后,才能启动服务。

  6. 首屏的依赖模块非常少,但是依旧要将整个项目进行打包。

  7. 使用路由懒加载等优化手段,但懒加载并不代表懒构建,也是需要把你的异步路由用到的模块提前构建好。 所以当项目越来越大的时候,模块越来越多,webpack每次处理的文件也越来越多,导致启动速度变的越来越慢。

接下来我们看看vite是怎么解决上述问题的。

Vite利用了浏览器的原生 ES Module 支持,让浏览器接管了打包程序的部分工作:浏览器只需根据html文件中type="module"的script 标签按需加载模块。这样就不存在将文件提前处理。 Vite只需要在先本地开启一个server,用来处理浏览器请求,将每一请求对应文件的源码进行转换并按需提供源码。

Native ESM based dev server的问题

vite虽然解决了基于bundle的开发环境问题,但是vite也同样存在一些问题:

1、Transform性能问题:尽管不需要将模块打包成bundle,但是vite需要将每一个请求进行转换,实际上Transform的操作是十分昂贵的。

2、非ESM标准模块兼容问题:对于.tsx,.jsx等文件的兼容问题。

3、Broswer ESM不能加载Node模块:对于node_modules下的模块,浏览器是没办法直接导入的。因为ESM规定:关键字from,后面是模块文件的路径。模块文件的路径是相对于站点根目录的相对路径。

import React from 'react';

4、Node模块自身问题:目前主流packages大多数只支持commonjs模块以及Node模块文件较多,大多数是一个文件一个请求(lodash)

Vite整体流程

整体流程分为三个阶段:启动阶段,运行阶段,HMR阶段。

启动阶段

在启动阶段,会触发依赖预构建,这样做的好处是:

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

2、性能: Vite 将有许多内部模块的 ESM 依赖关系转换为单个模块,以提高后续页面加载性能。

运行阶段

所以针对

1、Transform性能问题:

  • 模块转换时尽可能使用性能高的工具,比如esbuild, swc。
  • 利用HTTP请求缓存和Server缓存Transforms的结果。

2、Broswer ESM不能加载Node模块:

  • es-module-lexer扫描import语法
  • magic-string重新Node模块的导入路径
// 源文件
import React from 'react';
// 改写
import React from "node_modules/.vite/react.js?v=260ea0eb";

ESM HMR阶段

在使用过程会发现,我们明明在代码中没有使用import.meta.hot,但有时候热更新生效了。这是因为Vite将:

1、针对样式文件:在满足是开发环境,且请求的url的query中不含有direct或者inline,会自动注入样式相关热更新代码。

2、针对module:在非ssr得前提下,vite对自动根据是否是.jsx,.tsx或者导入react来自动注入react热更新相关代码。

Vite plugins

使用Vite插件可以扩展Vite能力,比如解析用户自定义的文件输入,在打包代码前转译代码,或者查找第三方模块。

插件执行顺序

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

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

插件钩子函数

在开发中,Vite 开发服务器会创建一个插件容器来调用钩子函数。

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

  • config
  • configResolved
  • configureServer
  • options
  • buildStart

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

  • transformIndexHtml
  • resolveId
  • load
  • transform

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

  • buildEnd
  • closeBundle

插件钩子调用顺序

插件例子

const reloadElectornPlugin = (): VitePlugin => {
  let configEnv: ConfigEnv;
  return {
    name: "reload-electorn-plugin",
    config(conf, env) {
      configEnv = env;
    },
    transform: (code: string, id: string) => {
      const opts = {
        excludes: ["electron"],
      };
      if (configEnv.command !== "serve") return code;

      const parsed = path.parse(id);
      if (!extensions.includes(parsed.ext)) return code;
      // 后续换成 es-module-lexer + magic-string
      const node: any = acorn.parse(code, {
        ecmaVersion: "next",
        sourceType: "module",
      });

      let codeRet = code;
      node.body.reverse().forEach((item) => {
        if (item.type !== "ImportDeclaration") return;
        if (!opts.excludes.includes(item.source.value)) return;

        const statr = codeRet.substring(0, item.start);
        const end = codeRet.substring(item.end);
        const deft = item.specifiers.find(
          ({ type }) => type === "ImportDefaultSpecifier"
        );
        const deftModule = deft ? deft.local.name : "";
        const nameAs = item.specifiers.find(
          ({ type }) => type === "ImportNamespaceSpecifier"
        );
        const nameAsModule = nameAs ? nameAs.local.name : "";
        const modules = item.specifiers
          .filter(({ type }) => type === "ImportSpecifier")
          .reduce((acc, cur) => acc.concat(cur.imported.name), []);
        
      codeRet = `${statr}const { ${modules.join(", ")} } = require(${
        item.source.raw
      })${end}`;
      });
      return codeRet;
    },
  };
};

名词解释

1、bare import:是指从node模块导入的模块且命名规范/^\w@/。eg:@tofft/react或者react。

2、boundaris:vite HMR里面更新文件向上冒泡查找,第一个含有import.meta.hot的模块。

3、import.meta:import.meta是一个给JavaScript模块暴露特定上下文的元数据属性的对象。它包含了这个模块的信息,比如说这个模块的URL。

4、module、chunk、bundle:我们直接写出来的是module,webpack处理时是chunk,最后生成浏览器可以直接运行的 bundle。