vite是什么
Vite 是一个由原生ESM 驱动的 Web 开发构建工具。在开发环境下基于浏览器原生ES imports 开发,在生产环境下基于Rollup打包。
它主要具有以下特点:
- 快速的冷启动
- 即时的模块热更新
- 真正的按需编译
Vite vs Webpack
在浏览器支持ESM之前,JavaScript 并没有提供原生机制让开发者以模块化的方式进行开发。这也正是我们对 “打包” 这个概念熟悉的原因:使用工具抓取、处理并将我们的源码模块串联成可以在浏览器中运行的文件。然而,当我们开始构建越来越大型的应用时,传统打包工具需要处理的 JavaScript 代码量也呈指数级增长。我们开始遇到性能瓶颈 —— 使用 JavaScript 开发的工具通常需要很长时间(甚至是几分钟!)才能启动开发服务器,即使使用 HMR,文件修改后的效果也需要几秒钟才能在浏览器中反映出来。而Vite 旨在利用生态系统中的新进展解决上述问题。
基于开发环境的数据
冷启动时间 | 二次启动时间 | HMR时间 | |
---|---|---|---|
Vite | 4207ms | 833ms | 95ms |
Webpack | 32960ms | 29466ms | 5535ms |
开发环境编译模式
传统的bundle模式,以Webpack为例:
从上图我们可以看出webpack打包分为以下步骤:
-
查找入口文件:从webpack的配置文件中查找entry的配置,从而找到入口文件。
-
分析依赖关系:接到入口文件之后,从入口文件出发,分析入口文件中依赖了哪些文件,并且这些依赖的文件中还可能依赖别的文件,就这么递归的找下去。
-
输出bundle:找到依赖中的所有文件,把这些文件转化成模块的函数,生成一个一个浏览器可执行的bundle。
-
启动服务:node创建本地服务器并启动静态页面。 从整个流程来看,它的问题是:
-
dev serve必须等到所有的模块打包成后,才能启动服务。
-
首屏的依赖模块非常少,但是依旧要将整个项目进行打包。
-
使用路由懒加载等优化手段,但懒加载并不代表懒构建,也是需要把你的异步路由用到的模块提前构建好。 所以当项目越来越大的时候,模块越来越多,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。