vite原理浅析-dev开发环境(对比webpack)
vite webpack 构建工具
首先 vite 不是打包工具,可以理解为一个项目开发启动器和打包工具启动器,分别对应 dev 的开发环境和 prd 的生产环境。
- 开发环境的特点是能够解析模块依赖,sourcemap,快速热更新。
- 生产环境的特点是丑化压缩依赖(尽可能减小体积),按需加载,浏览器兼容,将多个本地文件打包成一个 bundle 或多个第三方依赖打包成一个 vender(减少请求及更快的解析),及一些优化项等等
可以发现,dev 和 prd 环境 其实功能差异很大,所以分为两篇
首先 vite 之前的 vue、react 脚手架都一样,底层是基于 webpack 实现的,dev 对应 webpack-dev-server,prd 对应 webpack
webpack-dev-server 如何让浏览器能够解析模块依赖呢?
问题背景:比如你在源代码内写的 require(‘xx’),浏览器是不认识 commonjs 语法的。即使浏览器比较新可以解析 esm,但浏览器不知道要去 node_modules 里去找依赖,比如这种import vue from 'vue' 的写法。所以解析 cjs 或 esm 的语法 都是 webpack 来帮我们处理的,最终打包成一个 bundle,用 script 标签来加载,这样就可以解决兼容问题了
查看一个 webpack-dev-server 打包后的一个 vue 应用的入口 index.html
- 可以发现做法是:把所有依赖(包括第三方库)打包成一个 bundle(main.cb1fd56.js,项目不算大,体积 11Mb),然后通过 script 标签来访问
- main.cb1fd56.js 里面有很多 webpack 的 api 及代码注入
<!DOCTYPE html>
<html>
<head>
...
</head>
<body>
<div id="app"></div>
<script type="text/javascript" src="/js/main.cb1fd56.js"></script>
</body>
</html>
sourcemap 方面
因为是打包过的 多个文件合成了 1 个,所以 sourcemap 需要额外处理,才方便定位到具体的文件的具体行数,所以 webpack 的文件打包出来会非常大, 此处 vite 会简单很多
热更新方面
当项目变大后,热更新会越来越慢(因为每次都要重新打包),项目的启动速度也会越来越慢。vite 在热更新方面有极大的提升
进入正题,浅析 vite 原理
准备以下几个方面入手
- vite 基本原理-浏览器原生支持 esm
- vite 热更新和 dev 构建为什么快?
- vite 热更新是如何处理的?
- sourcemap 是如何处理的?
- vite 对源文件做的转换和处理?(比如让浏览器知道去 node_modules 里面去找依赖)
- vite 第一次构建为什么会慢?
- 为什么需要做依赖预构建?
- 公共依赖部分的处理?(比如 a、b、c 3 个文件 同时依赖一个 x 包)
- 一些 webpack 用习惯的功能该如何处理?(比如某些 webpack api,loader,plugin)
vite 基本原理-浏览器原生支持 esm
vite 基本原理是-浏览器原生支持 esm 好处:可以不用打包(热更新快的原理),浏览器直接可以识别 esm 模块(import 导入 export 导出)
- 注意点:不能识别 commonjs(require、module.exports)。所以源代码内都要用 esm 开发
- esm 的好处是可以做 tree shaking,让源代码的体积变得更小,也是 js 未来的主流。
- 过去的年代,js 只能用 script 标签引入模块,后面有了一些“hack”包,可以支持类似 require 的写法,比如 amd 模块。在后来 commonjs 标准借助 node 诞生,commonjs 还是有缺点,是运行时的,动态的,不支持 tree shaking
vite 热更新和 dev 构建为什么快?
首先快慢是看和谁对比,此处一般指的是和 webpack 的 dev-server 对比。
webpack-dev-server 慢的原因是,每次热更新都要重新打包。当我们开始构建越来越大型的应用时,需要处理的 JavaScript 代码量也呈指数级增长。包含数千个模块的大型项目相当普遍。我们开始遇到性能瓶颈 —— 使用 JavaScript 开发的工具通常需要很长时间(甚至是几分钟!)才能启动开发服务器,即使使用 HMR,文件修改后的效果也需要几秒钟才能在浏览器中反映出来。
vite 不用打包,只需处理一些源代码的路径问题(后面会讲)和预构建(后面会讲),所以几乎可以秒开 和 快速热更新,并且项目热更新的速度不会随项目变大而变慢,因为 vite 不用重新打包,只需更新对应的文件即可
vite 热更新是如何处理的?
大致过程和 webpack-dev-server 的差不多,但是 vite 可以不用打包,只需要更新修改过的文件即可
过程:
- 起 2 个服务:客户端服务,websocket 服务,
- 监听本地文件变更,区分变更的类型,然后做相应的处理,通过 ws 通知客户端服务,客户端服务在去加载新的资源或刷新网页
- 热更新时,不同的文件有不同的处理方式
- 当你只修改了 script 里的内容时:不会刷新,Vue 组件重新加载(会重新走生命周期)
- 当你只修改了 template 里的内容时: 不会刷新,Vue 组件重新渲染(不会重新走生命周期)
- 样式更新,样式移除时:不会刷新,直接更新样式,覆盖原来的
- js 文件更新时:网页重刷新
sourcemap 是如何处理的?
分为 2 块
- 用户写的源代码,vite 不会对进行打包,所以 vite 的 sourcemap 要简单很多
- dev 环境下 vite 解析出来的单个.vue 文件 几乎和原代码一样大,而 webpack 因为要注入代码和 sourcemap 和热更新代码,所以会很大。一个几 kb 的.vue 文件,webpack dev 环境去访问,至少会有 70 多 kb 的大小!
- 第三方依赖的话,在预构建阶段,借助 esbuild 打包好(sourcemap 有独立开源库处理)
vite 对源文件做的转换和处理(比如让浏览器知道去 node_modules 里面去找依赖)
贴一段 vite 解析后的 main.js 的代码,就会很清楚
- vite 会去转换依赖的路径,让浏览器可以通过文件路径访问到
// vite处理前
import { createApp } from 'vue'
import Antd from 'ant-design-vue'
import 'ant-design-vue/dist/antd.less'
import './assets/css/common2.less'
import './permission'
import { i18n } from './i18n'
import App from './App.vue'
...
// vite处理后
import { createApp } from '/node_modules/.vite/vue.js?v=9c1d7fbb'
import Antd from '/node_modules/.vite/ant-design-vue.js?v=9c1d7fbb'
import '/node_modules/_ant-design-vue@3.0.0-beta.5@ant-design-vue/dist/antd.less'
import '/src/assets/css/common2.less'
import '/src/permission.js'
import { i18n } from '/src/i18n.js'
import App from '/src/App.vue'
...
vite 第一次构建为什么会慢?碰到大模块怎么处理?(例如有上百个模块的组件库)
第三方依赖多一点的项目的小伙伴,应该能发现,vite 的第一次构建,其实是不快的。但第一次之后,以后重启项目都会很快,1s 内就可以完成。
- 第一次之后快的原因:
- 因为有缓存,缓存是放在/node_modules/.vite 文件内的。
- 如果某些时候碰到 依赖不更新,可以在 vite 命令后,加上—force,会自动删除 node_modules 的.vite 文件,然后重新构建
- 第一次构建慢的原因:vite 需要做 依赖预构建
为什么需要做依赖预构建?
以下 2 点来源于 vite 原文:
-
需要兼容 CommonJS 和 UMD:
开发阶段中,Vite 的开发服务器将所有代码视为原生 ES 模块。因此,Vite 必须先将作为 CommonJS 或 UMD 发布的依赖项转换为 ESM。当转换 CommonJS 依赖时,Vite 会执行智能导入分析,这样即使导出是动态分配的(如 React),按名导入也会符合预期效果:
// 符合预期
import React, { useState } from "react";
-
性能: Vite 将有许多内部模块的 ESM 依赖关系转换为单个模块,以提高后续页面加载性能。
一些包将它们的 ES 模块构建作为许多单独的文件相互导入。例如,
lodash-es有超过 600 个内置模块!当我们执行import { debounce } from 'lodash-es'时,浏览器同时发出 600 多个 HTTP 请求!尽管服务器在处理这些请求时没有问题,但大量的请求会在浏览器端造成网络拥塞,导致页面的加载速度相当慢。通过预构建
lodash-es成为一个模块,我们就只需要一个 HTTP 请求了!
简单总结成 2 点:
- 碰到大模块怎么处理?(例如有上百个模块的组件库)vite 需要做预解析,打包成一个 bundle(减少请求,否则要请求上百个模块)
- 预构建是使用 esbuild 打包的, esbuild 使用 Go 编写,并且比以 JavaScript 编写的打包器预构建依赖快 10-100 倍。
- 依赖也通常会存在多种模块化格式(例如 CommonJS 或者 UMD),需要通过 esbuild 将依赖打包为 ESM(浏览器只能识别 ESM 模块)
公共依赖部分的处理(比如 a、b、c 3 个文件 同时依赖一个 x 包)
- 如果是 esm 的依赖包,dev 环境中,浏览器会自动处理,公共的 x 包只会下载和解析一次
- 如果是 cjs 或 umd 的包,vite 会有一个预构建的过程,会先把他们转成 esm 包,然后同上
一些 webpack 用习惯的功能该如何处理?(比如某些 webpack 提供的 api,loader,plugin)
比如有小伙伴想迁移之前 webpack 的项目到 vite,需要注意的点:
- webpack 自身提供的 api(
https://webpack.docschina.org/api/module-methods/#require), vite 肯定是不支持的 - vite 中不支持 cjs(不支持 require 等模块导入导出的方法),还有 AMD 的 define 语法,需要转成 esm.(第三方依赖的话,vite 会自动转成 esm)
- 浏览器已经原生支持 import 动态导入(能支持
() => import('xx')),所以统一把 webpack 中可能有的写法 require.ensure 都改成 import():https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Statements/import- 此处有一点区别,webpack 提供的 import() 除了能加载 esm 外,还能加载非标准 esm(比如 cjs,umd 等),是 webpack 自己做的处理。vite 则用的是原生的 import() 只支持标准 esm 模块 (2 者都支持代码拆分,prd 是 rollup 来支持)
- 导入多个模块的话:vite 有一个
import.meta.glob的方法,可以从文件系统导入多个模块 :https://cn.vitejs.dev/guide/features.html#glob-import (webpack 会用自己实现的 api,这里要换成 vite 支持的 api)
loader 方面:
- vite 没有 loader 的接口,基本功能都内置好了
- css 的处理 和 预处理器部分,都内置好了,如果是要支持预处理器的话 需要安装对应依赖
# .scss and .sass
npm install -D sass
# .less
npm install -D less
# .styl and .stylus
npm install -D stylus
如果是用的是单文件组件,可以通过`<style lang="sass">`(或其他预处理器)自动开启。
Vite 为 Sass 和 Less 改进了`@import`解析,以保证 Vite 别名也能被使用。另外,`url()`中的相对路径引用的,与根文件不同目录中的 Sass/Less 文件会自动变基以保证正确性。
- dev 的 css,最终会做为 style 标签,放在 head 内
- 图片和字体部分,作为静态资源处理。类似 file-loader,处理链接就好了
- 比如本地的图片资源,源代码内,写的是相对路径(../assets/image/hll.png)或别名(@/assets/image/hll.png),最终会被 vite 解析成 服务器可以找到的 准确的路径,如:
<img src="/src/assets/image/hll.png">
- 比如本地的图片资源,源代码内,写的是相对路径(../assets/image/hll.png)或别名(@/assets/image/hll.png),最终会被 vite 解析成 服务器可以找到的 准确的路径,如:
- 更加定制化的需求
- 可能暂时 webpack 更合适,比如,你想支持 .myVue 或 .myReact 的文件,这种情况,可以写 webpack 的 loader 的来支持。
plugin 方面:
- dev 是开发环境,不需要做打包优化,所以 dev 几乎不需要写 plugin。更多的需求在 prd 环境用 rollup 打包,vite 能支持插件的编写:https://cn.vitejs.dev/guide/api-plugin.html ,也可以写 rollup 插件 (此部分会重点在 下一篇在讲)
总结
本篇描述的是 dev 模式下的 vite
- vite 基本原理是无需打包,依赖浏览器原生支持 esm 去解析模块。
- 不过需要高版本浏览器,chorme 都要 61 以上,支持动态 import 要 chorme63 以上
- 构建速度和热更新速度都非常非常快,热更新速度不会随项目变大而变慢
- 但第一次构建还是需要时间,之后就很快。第一次会做依赖预构建,用的 esbuild(速度很快)
为什么 webpack 做不到,webpack 必须得打包,并且打包出来的结果很丑,结构很乱 有很多注入代码?
- 因为 webpack 诞生在 es6 出来之前,当时没有 esm 的概念,webpack 只能自己实现 类似 require 的功能函数,并且利用 iife + 函数作用域,确保模块之前的作用域不会冲突