深度测评次世代打包工具Vite

  随着我们项目代码模块越来越多,打包和启动调试服务器所需要的时间也呈指数级增长,Vite是尤大大在推出Vue3时顺带推出升级的一个web构建工具,旨在解决构建慢的问题,那我们就来看一下,它构建有多快以及是如何构建的。

介绍

  Vite对其自身的定义为:

下一代前端开发与构建工具

  在深入对比Webpack、Parcel、Rollup打包工具的不同一章中,我们分别详细的对比了Rollup、Parcel和Webpack之间的异同,也分析了每个打包工具使用的场景;那么Vite作为次时代的打包工具,我们先来看下它的优点:

  • 极速的服务启动:使用原生 ESM 文件,无需打包!
  • 轻量快速的热重载:无论应用程序大小如何,都始终极快的模块热重载(HMR)
  • 丰富的功能:对 TypeScript、JSX、CSS 等支持开箱即用。
  • 优化的构建:可选 “多页应用” 或 “库” 模式的预配置 Rollup 构建
  • 通用的插件:在开发和构建之间共享 Rollup-superset 插件接口。
  • 谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里

  • 完全类型化的API:灵活的 API 和完整 TypeScript 类型。

  我们看到Vite主打的特点就是极速服务启动,也就是一个字:快!俗话说得好,天下武功,无坚不摧,唯快不破;我们先来搭建第一个项目看下,通过--template来指定预设的模板:

1
2
3
4
5
# npm 6.x
npm init @vitejs/app vite-vue-app --template vue

# npm 7+, 需要额外的双横线:
npm init @vitejs/app vite-vue-app -- --template vue

  Vite还支持以下模板预设:

  接着我们运行npm run dev或者yarn dev来启动服务器,可以看到服务器很快就启动了,不到400毫秒:

Vite启动时间

  Vue预设模板的项目结构大概如下:

Vite项目结构

谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里

  可以发现Vue预设模板的目录结构和vue-cli很相似,不同的是index.html文件的位置和配置文件,vite是vite.config.js,而不是vue.config.js

  最后就是我们的package.json,来看下vite需要哪些依赖:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
"name": "vite-vue-app",
"version": "1.0.0",
"scripts": {
"dev": "vite",
"build": "vite build",
"serve": "vite preview"
},
"dependencies": {
"vue": "^3.0.5"
},
"devDependencies": {
"@vitejs/plugin-vue": "^1.2.1",
"@vue/compiler-sfc": "^3.0.5",
"vite": "^2.1.5"
}
}

  可以看到Vite的依赖非常简单,默认支持Vue3.0,@vitejs/plugin-vue@vue/compiler-sfc都是Vue3.0的编译插件;如果想要支持Vue2.x,需要安装vite-plugin-vue2插件。

谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里

原生ES模块

  我们知道vue-cli的页面模板是在/public/目录下,那么来看下vite根目录下index.html有什么不同的地方:

1
2
3
4
5
6
7
8
9
10
11
12
13
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite App</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

  我们发现这里多引用了一个main.js,并且还有一个type="module"属性,那么这个属性有什么用呢?我们都知道我们代码中引用的ES模块必须要通过打包工具比如webpack等进行处理后才能在浏览器中进行使用;但是一些主流浏览器(Chrome、Edge、Safari、Firefox等)都在尝试原生支持ES模块,这是一个主要的新特性,也就是说我们在浏览器里就能直接使用ES的模块,不过让我们来看下浏览器对这个新特性的支持情况:

浏览器支持情况

  我们可以写个demo在浏览器上进行测试:

1
2
3
4
5
6
7
8
9
10
11
//count.js
export function add(num1, num2){
return num1 + num2
}
export function sub(num1, num2){
return num1 + num2
}
//index.js
import { add, sub } from './count.js'
console.log(add(1, 3))
console.log(sub(4, 2))

  我们先定义好es模块,然后需要通过script的方式进行引用:

1
2
3
4
5
6
7
8
9
10
11
12
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title></title>
</head>
<body>
<script type="module" src="/static/js/count.js"></script>
<script type="module" src="/static/js/index.js"></script>
</body>
</html>

  原生es和普通js脚本有写不同:

  • es module默认使用严格模式
  • es module有自己的作用域,使用var并不会创建全局变量
  • export和import关键字仅可在es module中使用
  • es module只会被浏览器解析并执行一次,普通js脚本每次引入都会解析执行
  • es module有跨域限制

谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里

  我们都知道,js的加载解析默认是会阻塞浏览器的,因此script标签一般都放在页面底部;但是我们可以给script加上defer来让js并发加载执行:

JS加载顺序

  我们发现原生ES模块和加上defer属性的效果是一样的;vite利用了浏览器对原生ES模块的支持,跳过打包(no bundle)过程,将ES模块解析编译后直接提供给浏览器;只在必要请求时进行代码转换,这样自然就节省了费时费力的打包时间。

  例如我们在请求首页home.vue模块时,只有在浏览器请求home.vue才将vue文件的template等解析编译,解析成浏览器可以执行的js返回。

  我们看下官方给出的传统打包工具的打包过程,从入口一直解析庞大的模块,然后打包成bundle,最后才能启动服务器:

传统打包

  但是Vite可以直接启动服务器,加载入口的文件:

谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里

Vite打包

  灰色部分的是暂时没有用到的模块,初始化不会参与构建,随着项目的路由越来越复杂,构建速度也不会变慢。

依赖预构建

  当我们首次运行项目时可能会发现下面的提示小字:

模块预构建

  我们上面说过Vite是不依赖构建的,那这里为什么还需要预构建呢?这里官方给出了两个原因:

谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里

  1. 兼容CommonJS和UMD:由于Vite服务器会将所有的模块当作原生ES模块在浏览器中使用,因此有一些依赖使用的CommonJS和UMD规范需要进行转换
  2. 提升性能:有些依赖包将内部的ESM模块拆分多个,预构建将多个模块转换为单个模块,提升页面加载性能

  兼容CommonJS和UMD不用多说,就是为了模块引用规范的统一,对模块化规范不了解的童鞋可以看这篇深入学习CommonJS和ES6模块化规范;提升性能官方给了一个现成的案例就是lodash-es依赖,当我们import { chunk } from "lodash-es",由于内部有600+个模块,相互导入,因此浏览器会去同时加载600+个http请求,虽然每个请求只有1~2kb,但是大量的请求也会造成网络堵塞;我们可以将lodash-es剔除预构建来看下效果:

1
2
3
4
5
6
import { defineConfig } from "vite";
export default defineConfig({
optimizeDeps: {
exclude: ["lodash-es"],
},
});

  我们在vite.config.js中修改optimizeDeps.exclude,然后就能在浏览器中看到效果:

lodash大量请求

  我们看到虽然请求数据不多,但是架不住600+大量的请求,正所谓乱拳打死老师傅;如果通过预构建将这些模块都统一到一起,那么速度快了不是一点点;那么什么样的模块会进行预构建呢?

  默认情况下,Vite会将package.json中生产依赖dependencies的部分启用依赖预编译,即会先对该依赖进行编译,然后将编译后的文件缓存在内存中(node_modules/.vite文件下),在启动DevServer时直接请求该缓存内容

谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里

环境变量

  很多情况下我们需要对打包的变量根据环境进行区分,比如请求的域名等,和vue-cli一样,vite也可以区分打包环境,不过它的变量比较特殊;我们知道它并不是通过webpack的DefinePlugin方式来定义全局变量,因此不能通过process.env来获取;而是通过一个特殊的import.meta.env对象来暴露,这个对象有一些公共的内在变量:

  • import.meta.env.MODE:运行模式,通过--mode来设置
  • import.meta.env.BASE_URL:部署的公共基础路径,由config文件中的base确定
  • import.meta.env.PROD:boolean值,是否运行在生产环境
  • import.meta.env.DEV:boolean值,是否运行在开发环境 (永远与import.meta.env.PROD相反)

  Vite支持dotenv,可以从项目根目录的文件加载额外的环境变量:

1
2
3
.env      # 所有环境都加载
.env.test # 测试环境
.env.prod # 正式环境

  加载的变量也会通过import.meta.env暴露给客户端代码,不过为了防止变量泄露,只有VITE_为前缀的变量才会暴露。

  这里引入一个模式的概念,默认情况下serve命令运行开发模式(development),而build命令会运行生产模式(production),但是我们可以通过env文件定义自己需要的模式;可以通过--mode选项覆盖命令使用的默认模式。

  比如我们项目在测试和正式环境之外可能还会设置一个预发环境,将一些线上的数据拷贝过来以便模拟真实场景,我们就可以定义一个staging模式:

1
vite build --mode staging

  我们可以将用到的环境配置放入.env.staging文件:

谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里

1
2
3
4
# .env.staging
NODE_ENV=production
VITE_APP_TITLE=My App (staging)
VITE_HOST=https://xieyufei.com

  这样staging模式就会打包和生产环境类似的代码,但是环境变量却是staging模式的,我们就成功新增了一种模式。

vite配置

  当我们运行vite时,默认会解析项目目录下的vite.config.js地配置文件,基础的配置文件导出一个对象

1
2
3
4
//vite.config.js
export default {
// 配置
}

  但是我们一般会引入defineConfig帮手函数,这样在没有jsdoc的配合下,也能获取类型提示:

1
2
3
4
import { defineConfig } from 'vite'
export default defineConfig({
// 配置
})

  如果我们的配置文件还需要根据环境或者模式的的不同来传递不同的插件或配置等,可以通过导出一个函数的方式:

1
2
3
4
5
6
7
8
9
10
11
export default ({ command, mode }) => {
if (command === 'serve') {
return {
// serve 独有配置
}
} else {
return {
// build 独有配置
}
}
}

谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里

css预处理

  对css预处理,Vite提供了开箱支持,我们只要安装对应的预处理依赖,无需配置,即可进行使用:

1
2
3
4
5
6
7
8
# .scss and .sass
npm install -D sass

# .less
npm install -D less

# .styl and .stylus
npm install -D stylus

  这样我们就可以通过<style lang="sass">自动开启预处理了;针对postcss的功能,我们可以直接配置postcss.config.js,它会自动应用所有的css,不过也需要安装对应的插件。

resolve解析

  和webpack类似,resolve字段用来表示如何来解析模块,首先我们看下常用的别名设置alias

1
2
3
4
5
6
7
export default defineConfig({
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),
},
},
});

  这样我们就能用@替换相对路径了,可以通过@来引入组件:

1
import HelloWorld from '@/components/HelloWorld.vue'

  还有mainFields,主字段解析,标志vite默认从模块的package.json中哪个字段引入模块,默认配置是:

1
2
3
4
5
export default defineConfig({
resolve: {
mainFields: ['module', 'jsnext:main', 'jsnext']
},
});

谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里

server服务器

  Vite提供了server选项来配置开发服务器,默认情况只允许localhost访问,我们可以指定server.host来让局域网主机也能访问:

1
2
3
4
5
6
export default defineConfig({
server: {
host: "0.0.0.0",
port: 3001,
},
});

  如果我们想让服务器启动时自动在浏览器中打开应用程序,可以配置server.open,配置类型boolean|string,配置为字符串时,会被用作 URL 的路径名:

1
2
3
4
5
export default defineConfig({
server: {
open: '/other-page'
},
});

  和webpack一样,我们可以通过server.proxy来为开发服务器配置代理,如果key值以^开头,则会被解析为正则表达式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
export default defineConfig({
server: {
// 字符串简写写法
'/foo': 'http://localhost:4567/foo',
// 选项写法
'/api': {
target: 'https://xieyufei.com',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '')
},
// 正则表达式写法
'^/fallback/.*': {
target: 'http://jsonplaceholder.typicode.com',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/fallback/, '')
}
},
});

谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里

按需引入

  我们在使用element时,经常会需要按需引入组件,在vue-cli中使用的是babel的一个插件babel-plugin-component;vite有自己的按需引入插件vite-plugin-style-import,首先我们安装一下:

1
npm install vite-plugin-style-import -D

谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里

  然后在vite.config.js中进行配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import styleImport from "vite-plugin-style-import";
export default defineConfig({
plugins: [
vue(),
styleImport({
libs: [
{
libraryName: "element-plus",
esModule: true,
ensureStyleFile: true,
resolveStyle: (name) => {
return `element-plus/lib/theme-chalk/${name}.css`;
},
resolveComponent: (name) => {
return `element-plus/lib/${name}`;
},
},
],
}),
],
});

  接下来如果我们只希望引入部分组件,就可以在main.js中加入:

1
2
3
4
5
import { ElButton, ElSelect } from 'element-plus';

const app = createApp(App)
app.component(ElButton.name, ElButton);
app.component(ElSelect.name, ElSelect);

本网所有内容文字和图片,版权均属谢小飞所有,任何媒体、网站或个人未经本网协议授权不得转载、链接、转贴或以其他方式复制发布/发表。如需转载请关注公众号【前端壹读】后回复【转载】。