当我们在谈vite时,我们在谈什么
前言
在ES6之前,JavaScript没有一个标准的模块方案,社区比较流行的是AMD方案和node使用的CommonJS的方案。
为了能够在浏览器端使用npm上大量的CommonJS规范的包,需要我们去对模块进行兼容和处理,这就是前端打包需要解决的一个问题。
Webpack
Webpack是现在主流的打包工具,不管是蚂蚁的umi还是vue的vue-cli,底层都是基于Webpack来进行二次封装的。
Webpack 官方将它定位为一个 module bundler
,它通过定义的入口文件,进行依赖收集,构建出项目的依赖图,最后生成并输出一个或多个bundle。
下图是官方给出的Webapck对于模块的处理流程
一个能在浏览器中运行的模块系统
我们希望浏览器能够顺利的运行第三方的包和业务代码,不管是commonjs、requirejs或者是es6的模块,所以需要提供一个新的模块系统,将这些第三方包和项目里的业务模块进行统一处理,以便在浏览器中能正确的运行。
webpack 用类似于node 的commonjs的模块方案,将最终打包的代码运行在浏览器中。
(function(modules) { // webpackBootstrap
// 已经加载的模板
var installedModules = {};
// 模块加载函数
function __webpack_require__(moduleId) {
// ...
}
// 入口文件
return __webpack_require__(__webpack_require__.s = "./index.js");
})
({
"./index.js": (function(module, __webpack_exports__, __webpack_require__) {}),
"./utils/test.js": (function(module, __webpack_exports__, __webpack_require__) {})
});
上面就是webpack打包出来的代码,其中index.js是项目的入口文件,所以的模块都通过 __webpack_require__
进行加载,并且会把加载完成的模块进行缓存。
var installedModules = {};
// The require function
function __webpack_require__(moduleId) {
// Check if module is in cache
if(installedModules[moduleId]) {
return installedModules[moduleId].exports;
}
// Create a new module (and put it into the cache)
var module = installedModules[moduleId] = {
i: moduleId, // Module ID
l: false, //
exports: {}
};
// Execute the module function
modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
// Flag the module as loaded
module.l = true;
// Return the exports of the module
return module.exports;
}
通过__webpack_require_
的代码可以看出,webpack实现了一个类似CommonJS的模块方案。
插件系统
对于webpack来说,如果仅仅解决不同方案模块的聚合和运行,是远远不够的。对于越来越复杂的前端项目,需要提供更加底层的能力给开发者,去实现不同业务场景的不同需求。
Webpack 基于 Tapable 实现了一个复杂的插件系统,在它的构建过程中,对外抛出了各个关键的节点的hook,开发者可以通过订阅这些勾子,对webpack中的资源进行加工和处理,从而完成定制化的需求
通过Plugin和Loader,能够不断扩展Webpack的功能。
存在的问题
当然,Webpack 也不是十全十美的,在学习和使用过程中,也会存在大大小小的问题
- 概念和配置项太多,需要配合实际的项目场景不断的优化配置文件,umi、vue-cli等脚手架就是为了解决无法开箱即用的问题
- 随着项目的不断变大,devServer的启动时间会越来越长,并且hmr的速度也会受到影响
- 功能缺失,在webpack5之前,没有系统级别的文件缓存系统
契机的出现
契机一:http/2
由于http/2的普及,使得很多基于http/1的优化工作都变成了反模式,其中最主要的一条就是合并代码减少网络请求。
因为在http/1的时代,浏览器只能并行的发起5个请求,这就造成如果文件太多,会存在请求阻塞的问题,所以在很多项目中,我们都会去对代码进行打包生成尽量少的vendor。但是在http/2里,所有的请求都会在一个tcp链接里面完成,并且资源都是并行加载的,这时候单个大文件的加载时间反而会比多个小文件的时间要多。所以在http/2的网络里,我们需要对项目资源进行合理的拆分,充分利用资源的并行请求来减少资源的加载时间。
契机二:ES Module
在es6中,加入了JavaScript的模块。
只需要在script标签上加上 type=module
的属性,浏览器就会将内联代码或者外部引用的脚本视为ECMAScript模块。
相比于 Node 的 CommonJS 模块,ES Module 有很多的不同:
- ES module 抛出的是一个引用,而exports抛出的是一个值
- require 可以进行动态引用,而ES module需要在作用域的顶层声明所有的依赖,这就导致Node是可以在运行时去加载模块的, 而ES module可以在编译阶段就完成所有的依赖分析,并为后面的优化做准备(比如tree shaking)
- ...
下面的代码可以在浏览器中直接运行
// test.js
export default function hello() {
console.log('hello world');
}
// index.html
<script type="module">
import hello from './test.js';
hello(); // hello world
</scirpt>
在支持ES module的浏览器中,当解析到ES module模块的时候,浏览器就会自动发起一个请求去加载对应的模块资源,不再需要我们去处理模块的引入和加载。
对于ES module的支持,主流的现代浏览器已经有着不错的兼容性,随着使用者的升级,这个覆盖率会越来越高。
Vite
通过上面的介绍,我们似乎可以利用 http/2 和 浏览器对 ES module 的支持,来直接加载代码,而不再需要进行代码的打包。
Vite 和 snowpack 就是基于这种想法而诞生的前端构建工具。
Vite是什么
Vite,一个基于浏览器原生 ES imports 的开发服务器。利用浏览器去解析 imports,在服务器端按需编译返回,完全跳过了打包这个概念,服务器随起随用。同时不仅有 Vue 文件支持,还搞定了热更新,而且热更新的速度不会随着模块增多而变慢。针对生产环境则可以把同一份代码用 rollup 打。虽然现在还比较粗糙,但这个方向我觉得是有潜力的,做得好可以彻底解决改一行代码等半天热更新的问题。
-- 摘自尤雨溪微博
Bundleless
Vite 直接使用ES Module,利用浏览器去解析文件中import的依赖,对于使用到的依赖通过请求去获得相应的资源文件, 从而不再需要像Webpack那样,从入口文件出发,收集所有的依赖然后整体打包,最后把打包的bundle文件交给浏览器解析运行
上图是各个构建工具在打包生成最终的代码所需要的时间,其中snowpack和vite都是Bundleless的方案,所以在最后生成代码的时候不需要任何的构建,直接把ES module的代码抛给浏览器即可。
极速的hmr
Vite 不会因为模块数量的膨胀而造成热更新的速度变慢,因为 Vite 不像 Webpack 需要构建一份全量的依赖图,并且这份依赖图有可能在模块依赖特别复杂的时候会造成热更新的不准确
devServer使用304来进行协商缓存,对于已经加载过的模块,会增加Cache-Control: max-age=31536000 来做缓存,减少网络请求
上图是snowpack和webpack 热更新所需时间的对比
真正的按需编译
Vite 通过浏览器的解析来进行依赖资源的加载,所以在文件没有被使用之前,所有的依赖文件都不用进行处理,真正做到了按需编译
Webpack 的spa模式很难进行按需编译,因为从入口文件开始,会做完所有的依赖分析和处理,就算是进行了按需加载的模块,也还是会被编译。另一个可行的方案是mpa,但是就脱离了单页应用的开发模式了,并且项目在开发和发布的时候复杂度都会增加。
上面两张图片可以直观的看出Vite在编译上的优势,项目只需要在路由层面进行按需加载,就能够做到按需编译。
这里需要注意的是,如果项目没有对路由进行按需加载的处理,最后还是会一次性编译加载所有的依赖
极速的编译速度
Vite 使用 Esbuild 来进行代码的编译,其中jsx和tsx都是通过Esbuild解析生成AST,再生成最终的es module的代码,commonjs到ES module也使用它来进行转换
Esbuild是使用Go语言进行开发的,并且发布的是进行编译过的更底层的机器码,在执行效率上远远高于JavaScript编写的打包器,预期会快10-100百倍
在 Vite 1.0 中,使用 rollup 的 @rollup/plugin-commonjs 来实现commonjs转换成ES6,在Vite 2.0 中,使用esbuild来处理模块转换,大大提高了效率
上图是对10份there.js
的包进行打包时,各个构建工具所需要的时间,可以看出esbuild的优势十分巨大
依赖预构建
在 Vite 首次运行的时候,会从入口文件出发,对其中的第三方依赖进行分析和打包,并把这些chunk进行缓存,提高页面的加载速度
很多第三方的依赖都是commonjs和umd的模块,通过预构建,用 esbuild 将模块转换为ESM,这样这些第三方的包就能在浏览器中直接运行了
将多入口的模块,比如lodash,转换成单文件的模块,减少网络请求,提高页面的加载性能
开箱即用
除了对于Vue的第一优先级的支持,Vite 还在内部内置了大量默认的处理,比如支持jsx和tsx,样式预处理库支持less、sass 和 CSS module
完整的脚手架工具链,可以快速生成Vue、React等项目的模板,提供了大量的配置项,作者说后期有可能会替换掉vue-cli
和vue、vue-cli 一样优秀又友好的文档,并且有对应的中文文档
简单的处理流程
使用的核心依赖
- Connect & Connect middleware
- Rollup & Rollup plugin
- esbuild & esbuild plugin
- acorn、es-module-lexer ...
Vite 1.0 的版本中使用 koa 来作为devServer,并且通过koa的插件机制,使用不同的插件来处理各种格式的文件和各种拓展的功能
Vite 2.0 中使用Connect替代了koa,并通过中间件来进行流程控制,因为Vite使用Rollup来进行最终的代码build,所以直接拥抱了Rollup的开源社区, 在内部也继承了Rollup的插件扩展方案,通过Rollup插件来拓展Vite的功能
不足之处
- Esbuild 现在还不够稳定,无法用于生产环境的打包,项目打包的时候需要使用Rollup,这就造成开发环境和生产环境的代码不一致,有可能一些bug会无法定位
- 浏览器的支持度还有待提升
- ssr还在试验阶段
惯性思维导致的一些问题
在试用vite跑demo的时候,遇到了一些问题。
因为使用的技术栈是react + antd,所以就简单的搭了一个demo,来看一下实际的开发体验。在以往的开发中经验中,对于有antd的项目,做的第一件事可能就是引入 babel-plugin-import 实现模块的按需加载,在这个demo中也使用了相同的方案实现按需加载。
但是在项目本地运行以后,发现在没有缓存的第一性运行时,页面加载速度有时候会非常慢,通过network的瀑布图分析后,是因为每次页面进来的时候,都会加载使用到的antd模块。后来分析发现,是 babel-plugin-import 这个插件造成的。
babel-plugin-import的作用是做antd引入语法的转换,转换的效果如下
import { Button } from 'antd';
↓ ↓ ↓ ↓ ↓ ↓
var _button = require('antd/lib/button');
require('antd/lib/button/style/css');
复制代码
插件将antd的bare import的语法转换成绝对路径的引入方式,这就导致Vite对于antd 的预编译失效,因为预编译只对bare import的模块有效,并且如果当前路由依赖的antd的模块比较多,就会造成页面的加载速度比较慢
最近遇到的另一个babel-plugin-import造成的问题是组件库打包的问题。因为组件依赖了antd,在打包成umd包的时候,需要通过externals将antd、react和react-dom 排除出去,但是在分析最后打包产物的时候,发现react和react-dom没有打包进来,但是antd用到的模块还是被打包进来了。后来经过排查发现,在打包的babel配置中,使用了babel-plugin-import,因为loader的编译是在build构建之前完成的,所有的antd的bare import全被编译成了绝对路径的引入方式,导致webpack build的时候externals替换规则失效,因为externals也只能对bare import的模块进行替换。
所以惯性思维会对我们造成一定程度的困扰,需要我们更透彻的理解其中的原理和构建过程,才能避免很多坑。
总结
- 基于 ES Module 的前端构建工具,充分利用浏览器自身的能力,本质上解决本地开发项目构建时间长等问题
- 相比于Webpack,更加轻量,封装的层级更高
- 完整的生态,活跃的社区,稳定的核心开发
更多技术文章请关注公众号:字节逆旅