作者:Manz
前言
一直以来,我们所有用的都是以webpack为基础搭建的各类拿来即用的开发工具,比如
CRA
vue-cli(Command-Line Interface)
公司内部自己的脚手架。
但是自己配置过的同学都知道webpack有很多配置,比如entry,output,plugins,loaders。配置过程极其繁琐。而且随着使用的深度,整个项目的编译会越来越慢。所以就能看到随处可见的前端程序员按完Ctrl+S之后就会发呆一会,他们不是在摸鱼,而是在等待编译。
得益于浏览器对ES Module的支持,开始出现了基于浏览器原生ES imports的开发服务器:snowpack,vite对比。大大减少前端开发过程中的发呆时间。
下面,开始简单介绍vite。对vite介绍不感兴趣可以直接拉到最底下看如何改动。
Vite简介
Vite(法语单词,“快” 的意思)是一种新型的前端构建工具,开箱即用。主要由两部分组成:
一个开发服务器,它基于原生ESmodule提供了丰富的内建功能,如速度快到惊人的模块热更新(HMR)。
一套构建指令,它使用Rollup打包你的代码,并且它是预配置的,可输出用于生产环境的高度优化过的静态资源。
具有以下特点:
1. 开发效率极高
2. 开箱即用
3. 社区丰富,兼容rollup
4. 超高速热更新
5. 预设应用和类库打包模式,减少很多需要配置的内容
6. 前端类库无关(同时支持React,Vue)
作者原话:
Vite,一个基于浏览器原生 ES imports 的开发服务器。利用浏览器去解析 imports,在服务器端按需编译返回,完全跳过了打包这个概念,服务器随起随用。同时不仅有 Vue(vite2 同样完美支持React) 文件支持,还搞定了热更新,而且热更新的速度不会随着模块增多而变慢。针对生产环境则可以把同一份代码用 rollup 打。虽然现在还比较粗糙,但这个方向我觉得是有潜力的,做得好可以彻底解决改一行代码等半天热更新的问题。
对比dev server
传统模式:
Vite以原生ESM方式提供源码。这实际上是让浏览器接管了打包程序的部分工作:Vite只需要在浏览器请求源码时进行转换并按需提供源码。根据情景动态导入代码,即只在当前屏幕上实际使用时才会被处理。
代码热更新(由上图可知)
传统:
基于打包器启动时,重建整个包的效率很低。原因显而易见:因为这样更新速度会随着应用体积增长而直线下降。
Vite:
在Vite中,HMR是在原生ESM上执行的。当编辑一个文件时,Vite只需要精确地使已编辑的模块,使得无论应用大小如何,HMR始终能保持快速更新,极其稳定。Vite同时利用HTTP 头来加速整个页面的重新加载(再次让浏览器为我们做更多事情):源码模块的请求会根据 304 Not Modified 进行协商缓存,而依赖模块请求则会通过Cache-Control: max-age=31536000,immutable进行强缓存,因此一旦被缓存它们将不需要再次请求。
速度对比
Vite在开发环境中采用ESM的模块加载方式,并且尽可能剔除了不必要的代码构建,同时采用了激进的Esbuild–一个用go语言开发的JS编译工具,性能是webpack之类的10-100倍–来进行编译提速,这就让Vite启动速度极其迅速,基本3秒内就能完成。同时得益于ESM加载方式,Vite不需要提前进行代码打包,所以即便你的项目变得巨大,但是Vite并不关心你的文件规模,所以他的启动速度仍然不会有太多的变化。对于开发时的编译也类似。
速度快的一个重要原因:
预编译:
查看项目中node_modules下的 .vite(生成缓存的地方)
把非ESmodule编译成ESmodule,比如源码中的react的exports需要改成ESmodule。
只有当以下情况发生时,才会重新pre-bundle来更新缓存。
1. package.json的依赖
2. package-lock.json, yarn.lock, or pnpm-lock.yaml. 等
3. vite.config.js
速度对比的实例
有开发过经验的可以跟这个做一个对比,这是recipe用vite启动的时间,而用webpack则需要很久很久,好几分钟。
配置对比
Vite的配置很简单,因为其预设就是为前端项目开发而生,所以对于dev-server,css依赖,图片加载之类的功能其都配置成开箱即用,你完全不需要再去配置插件、loader之类的(但是需要安装其依赖)。所以很可能你的vite配置不会超过20行代码,这在webpack项目中基本不太可能。
Vite的缺点
Vite还很新,虽然它从理论与体感上提供了非常极致的开发体验,还是有一些值得关注的问题。
● 兼容性
默认情况下,无论是dev还是build都会直接打出ESM版本的代码包,这就要求客户浏览器需要有一个比较新的版本,这放在现在的行情下还是有点难度的。
不过Vite同时提供了一些弥补的方法,使用配置项配合@vitejs/plugin-legacy打包出一个看起来兼容性比较好的版本,这一点会随时间慢慢被抹平。
● 缺少 Show Case
Vite比较新,社区还没太反应过来,大型、复杂的商业落地案例少,谁都说不准这里面可能有多少坑。不过好消息是社区对Vite的搜索热度在最近几个月急剧增长。
● 虽然发展了有一阶段,但是还有极少部分的包还不支持ESM,很不幸的是公司常用包map-box就不支持,目前看官方的issue,似乎并没有得到很好的解决,感兴趣的同学可以尝试一起解决下这个棘手的问题。
● 不算缺点的缺点,因为是按需编译,所以会导致你在访问其他模块的时候,会比webpack已经编译过的慢上一点点(这里绝对没有主观意图)
了解ESmodule
前端社区之前有出一些非标准的模块管理方案:
CommonJs规范
AMD规范
UMD规范
而ES Module是随着ES6语法推出的正式的JavaScript模块语法. ES Module增加了import和export两个主要的关键字,并且使用特殊的语句来完成模块的功能:
这两个是关键字而不像require是一个函数,所以他们的使用具有一定的限制,这两者都只能在模块的顶级作用域中使用(所以我们需要使用vite的项目中,引入资源不能使用require):
在现在主流的现代浏览器上基本都已经支持了ES Module的JS模块管理功能。我们可以通过:
开发服务器
Vite提供了一个开发服务器,然后结合原生的ESM(由ESbuild 预构建的依赖),当代码中出现import的时候,发送一个资源请求,Vite开发服务器拦截请求,根据不同文件类型,在服务端完成模块的改写(比如单文件的解析编译等)和请求处理,实现真正的按需编译,然后返回给浏览器。请求的资源在服务器端按需编译返回,完全跳过了打包这个概念,不需要生成一个大的bundle。服务器随起随用,所以开发环境下的初次启动是非常快的。而且热更新的速度不会随着模块增多而变慢,因为代码改动后,并不会有bundle的过程。
Vite Server所有逻辑基本都依赖中间件实现。这些中间件,拦截请求之后,完成了如下内容:
1. 处理ESM语法,比如将业务代码中的import第三方依赖路径转为浏览器可识别的依赖路径;
2. 对.ts、.tsx 等文件进行即时编译;
3. 对Sass/Less的需要预编译的模块进行编译;
4. 和浏览器端建立socket 连接,实现 HMR。
生产构建rollup
尽管原生ESM现在得到了广泛支持,但由于嵌套导入会导致额外的网络往返,在生产环境中发布未打包的ESM仍然效率低下(即使使用HTTP/2)。为了在生产环境中获得最佳的加载性能,最好还是将代码进行tree-shaking(如果能做到都是用ESM,就能很好的做到tree-shaking)、懒加载和chunk 分割(以获得更好的缓存)。要确保开发服务器和生产环境构建之间的最优输出和行为一致并不容易。所以Vite 附带了一套构建优化的构建命令,开箱即用。也就是Rollup。
就目前来说,Rollup在应用打包方面更加成熟和灵活。虽然esbuild快得惊人,并且已经是一个在构建库方面比较出色的工具,但一些针对构建应用的重要功能仍然还在持续开发中——特别是代码分割和CSS处理方面。
vite打包能不能上生产,会不会出现不一致的情况?
● 开发环境下,模块以原生esm的形式被浏览器加载。
● 生产环境下,模块被Rollup以传统方式打包,而且做了很多默认优化。虽然默认是打包的格式也是ESM,但也可以通过plugin-legacy输出其它格式兼容旧浏览器。
● 开发和生产环境下共享同一套Rollup插件机制,所以单个模块的编译在开发和生产环境下是一致的。有些人担心一个打包一个不打包会产生不一致,这个理论上存在可能性 —— 本质上这依赖于Rollup的打包结果是否符合标准的ESM semantics,而Rollup是一个相当成熟的打包工具了,这一点上还是值得信赖的;另一方面,webpack 开发和生产环境下打包出来的代码也是完全不一样的(可以调不同的sourcemap配置查看),所以开发和生产环境不管是用什么工具都存在理论上的不一致问题,实际上只能以用的人够多并且没踩到坑为判断准则。
已有项目新增vite实践
1.新增html文件(后续可以直接用vite插件钩子transformIndexHtml直接转译html)。插件开发部分不讲。
这里需要注意给一个global对象,没给的话,因为环境的原因会有个别 node_modules 报错。
2. package.json
新增启动命令:"dev": "vite",
新增插件(注意对应的版本号):
3.图片引入方式:
// --> 改为
import test from "./asset/img/wordmark_white.png";
// ...
4. tsconfig.json 的修改配置
{
"compilerOptions": {
"skipLibCheck": true, // 这个不校验第三方库的声明,搭配 checker 使用
"target": "ES6",
"isolatedModules": true, // 必须有这个配置 将每个文件作为单独的模块(与“ts.transpileModule”类似)。需要删除无用的文件
"noEmit": true // 默认false 不生成输出文件。
},
}
5. 最终的配置 (仅限开发环境,编译环境还未实践)
import {defineConfig} from "vite";
import react from "@vitejs/plugin-react";
import path, {resolve} from "path";
import fs from "fs";
import styleImport from "vite-plugin-style-import";
import checker from "vite-plugin-checker"; // 加上这个checker之后速度变慢了。
// https://vitejs.dev/config/
const proxyContext = ["/image-uploader", "/video-uploader", "/test-site", "/test/excel", "/restaurant/excel", "/ajax", "/cms/file-uploader", "/file-uploader", "/file-download", "/image", "/simulator/excel"].reduce((prev, next) => {
prev[next] = {
target: "https://test.qa.com/",
changeOrigin: true,
secure: false,
};
return prev;
}, {});
const moduleRoot = "./src/";
const srcModules = fs.readdirSync(moduleRoot);
const paths = srcModules.reduce((prev, next) => {
const currentPath = `${moduleRoot}${next}`;
if (fs.statSync(currentPath).isDirectory()) {
prev[next] = path.join(__dirname, currentPath + "/");
}
return prev;
}, {});
export default defineConfig({
plugins: [
checker({ // 因为添加了这个check,整体编译实践翻倍了,由0.8s变为了1.6s
typescript: true,
eslint: {
files: ["./src"],
extensions: [".ts", ".tsx"],
},
}),
react({
babel: {
parserOpts: {
plugins: ["decorators-legacy"],
},
},
}),
styleImport({
libs: [
{
libraryName: "antd",
resolveStyle: name => `antd/es/${name}/style`,
},
],
}),
],
build: {
rollupOptions: {
output: {
// 这个时候会把所有的模块都区分开
// manualChunks(id) {
// if (id.includes("node_modules")) {
// return id.toString().split("node_modules/")[1].split("/")[0].toString();
// }
// },
// 类似webpack自己组装
manualChunks: {
lodash: ["lodash"],
antd: ["antd"],
},
},
},
},
css: {
preprocessorOptions: {
less: {
// 支持内联 JavaScript
javascriptEnabled: true,
modifyVars: {
"primary-color": "#A0006B",
"link-color": "#116EBE",
},
},
},
},
resolve: {
alias: {
...paths,
conf: resolve(__dirname, "conf/dev"),
},
},
server: {
host: "test.qa.com",
port: 6443,
https: true,
proxy: proxyContext,
},
});
建议迭代计划
1. 我们已有webpack本地开发环境和打包环境,此时我们可以在不影响当下webpack的情况下,加入vite开发环境,这时候是不需要动到webpack的。没有影响。
2. 我们已有webpack开发环境和vite开发环境,我们这里还需要进行vite打包的实验阶段。抽出一个sprint去观察打包结果是否正常,兼容性是否正常,理论上不会有问题,但是涉及到生产,还是得多花时间去观察。