宋秋晓

You are the JavaScript in my HTML

0%

写写 Vite

如果说让你对公司的新项目做一下前端的技术选型,在打包方面,你会想用什么?我相信,高票答案肯定是 Webpack。

几年前我在上家公司,一路从小白摸爬滚打做前端的路上,第一次接触到的打包工具也是 webpack,功能确实强大,生态环境也很好。一度以为打包要被 webpack 一统天下了。
不过后来换到当前公司,项目代码本身大了几十倍,需要处理的 javascript 代码量也呈指数级增长,本地起项目直接带不动,即便把一部分入口文件过滤掉,也要 build 个几分钟的,所以那段时间的工作状态就是,npm run start,然后去倒杯水,泡个茶(喂! 如此循环往复,迟钝的反馈会极大地影响开发者的开发效率和幸福感。
img
当然我们后来为了工作体验,也是各种删减代码内容、加缓存、拆分仓库等等各种工程优化,但是脑子里突然就想过,有没有考虑其他更高效的打包工具呢?也就是这个时候去网上翻了各种打包文件的对比,vite 应该算是里面比较出众的,这两年也很火。

vite 优势

当前 webpack 打包有两大痛点:

  • 服务器启动慢
  • 更新慢

webpack 会先打包 ,然后启动开发服务器,请求服务器时直接给予打包结果。 而 vite 是直接启动开发服务器,请求哪个模块再对该模块进行实时编译。这种按需动态编译的方式,极大的缩减了编译时间,项目越复杂、模块越多,vite 的优势越明显。在 HMR(热更新)方面,当改动了一个模块后,仅需让浏览器重新请求该模块即可,不像 webpack 那样需要把该模块的相关依赖模块全部编译一次,效率更高。

vite 通过在一开始将应用中的模块区分为 依赖源码 两类,改进了开发服务器启动时间。
依赖方面vite 将会使用 esbuild 预构建依赖。Esbuild 使用 Go 编写,并且比以 JavaScript 编写的打包器预构建依赖快 10-100 倍。因为 js 跟 go 相比实在是太慢了,js 的一般操作都是毫秒计,go 则是纳秒。
源码方面vite 以原生 ESM 方式提供源码。这实际上是让浏览器接管了打包程序的部分工作:vite 只需要在浏览器请求源码时进行转换并按需提供源码。根据情景动态导入代码,即只在当前屏幕上实际使用时才会被处理。
官方图解如下:

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

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

围观源码仓库

我去vitejs仓库上看了下,共有 5 个相关的项目:
img
其中 vite 是核心库,而 create-vite-app 是开发的脚手架工具,取名很像 create-react-app,通过集成 vite-plugin-react-pages 和 vite-plugin-react,vite 也能开发 react 项目。
由此看出尤大决心很大,不仅是希望依托于 vue 强大的生态环境让他大火,更希望包揽 react 的圈子,想让 vite 和 webpack 一较高下的意图很明显。

内部工作原理

还是根据上面的基于 ESM 的构建模式图片为基础,这里我们结合 react 进行原理举例剖析:

type=”module”

vite 首先会在本地帮你启动一个服务器,当浏览器读取到 index.html 这个入口文件时,会发现里面会用 type="module"的方式去加载文件。

1
<script type="module" src="/src/main.jsx"></script>

img
那给 script 标签设置成 type="module" 有什么好处呢?就是因为 vite 利用了浏览器原生 ES Module 的支持,会将这个脚本视为 ES 标准模块,然后以模块的方式去加载跟执行。

依赖预构建 ESModule

文件之间的互相导入解决了,但是我们的依赖管理是采用 npm 的,而 npm 包大部分是采用 CommonJS 标准并没有兼容 ES 标准。关于这一点 vite 官方文档也写了,在开发阶段中,会首选采用依赖预构建的过程。官方解释如下:

img
其中有一点,就是 Vite 的开发服务器会将所有代码视为原生 ES 模块,会将作为 CommonJS 或 UMD 发布的依赖项转换为 ESM。这样的话在 /src/main.jsx 文件中可以以 es 模块的方式来进行组织和编写,且不需要打包。

  1. 首先加载 main.jsx 文件:
1
2
3
4
5
6
7
8
9
10
11
import React from "react";
import ReactDOM from "react-dom";
import "./index.css";
import App from "./App";

ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById("root")
);
  1. 再依次发送加载 react.js、react-dom.js、index.css 和 App.jsx 文件

img
这些都加载过来之后,把它创建应用程序在页面中显示。这样做的好处有两点:

  • 在开发阶段不需要进行一个依赖构建的计算,不管我的项目有多大,我的速度都是最快的。
  • 按需加载,浏览器打开了什么页面,我就加载这个页面相关的文件,不相关的绝不加载,这样的话开发的速度就更好了。
  1. 三方模块解析

前面虽然说知道要加载 react.js、react-dom.js、index.css 和 App.jsx 这些文件,但是对于浏览器本身来说,其实只知道 index.css 和 App.jsx 这俩相对路径的文件的,那么 react.js、react-dom.js 怎么引入的呢?总不至于让浏览器下个 npm 吧,我们看看打包之后文件:
img
这就是预构建的好处,提前把需要的第三方依赖都放到node_modules/.vite下面,然后把引入重写成

1
import __vite__cjsImport0_react from "/node_modules/.vite/react.js?v=e34eeb44";

诶?有没有小伙伴发现?v=e34eeb44这个东西,这个用法倒是显而易见的,为了缓存,其实也就是前面提到的缓存策略
可以看到首次请求如下:
img
之后的请求就会走缓存:
img
如果文件发生了变化就会更新后面的 hash 来控制文件更新,把缓存的工作任务交给浏览器来做,还是很巧妙的。
然后不知道大家有没有发现一个事情,看下面的图,我的 chrome 从白色变成黑色说明我博客从白天写到了黑夜,啊不是,就是引入的文件,依然是 jsx 文件格式,那么浏览器到底是怎么识别的?
img
其实是 vite 做了额外的事情,仔细看文件的 content-type 其实是application/javascript,这说明这个文件已经被 vite 进行了解析编译,就好比 webpack 里 babel-loader 的功能,那么 vite 是依赖什么进行转换的呢,没错就是 esbuild。
img
img
vite 使用 esbuild 来作为部分文件类型的解析器,它的解析编译跟 webpack 是不一样的。webpack 是提前将所有文件编译为浏览器可以接受的类型,而 vite 则是在接收浏览器发起的 http 请求之后再去编译对应文件。
你可能会想,每次页面加载都需要编译一次文件,这对页面的加载速度考验极大呀!这里我们通过这张图比较发现,esbuild 的构建速度是敲快的(可以戳这里看)!是不是超心动的
img
不过正式环境上 vite 最终还是没采用 esbuild。解释见官方文档
以上差不多是我对 vite 的一些理解,新的项目用这个工具我可能会更适应。目前我司的代码配置(入口文件在后端代码里)直接去替换 vite 可能还不是很熟悉(其实写这篇博客的时候还是一直在顺便替换的,奈何遇到的问题太多,后面改动成功的话,说不定又是一篇新的博客呢!)

小结

个人感觉,如果是个新的 vue 项目,可以试试直接用 vite 来作为打包生产力;如果是老项目的话,建议用 vite 来作为开发环境生产力,提效还是杠杠的,但是生产环境还得是 webpack 更稳定一些。
不知道按照这个势头,最后 vite 会不会撼动 webpack 的地位呢,毕竟提升研发效率,是技术人永恒的追求。

-------------本文结束感谢您的阅读-------------
1