Vite从0到1
vite 初识
创建一个 vite 项目,只需要:
yarn create vite
然后按照提示进行操作即可(这里选的是 react + js)。生成的项目目录如下:
├─ public
│ └─ vite.svg
├─ src
│ ├─ assets
│ │ └─ react.svg
│ ├─ App.css
│ ├─ App.jsx
│ ├─ index.css
│ └─ main.jsx
├─ index.html
├─ package.json
└─ vite.config.js
这个命令先是安装一个全局依赖 create-vite
,然后运行 create-vite
命令。等同于:
cnpm install create-vite -g && create-vite
create-vite
就是一个 vite 的脚手架,会根据你的需要选择不同模板来克隆项目。
需要注意的是, vite 对 node 版本有要求, 要求 node 版本是 ^14.18.0 || >=16.0.0
。
可以看到根目录有个 index.html。这个是 vite 项目的入口文件。
Vite 解析 <script type="module" src="...">
,这个标签指向我们的 JavaScript 源码。
为什么选择 vite
Vite 是一种新型前端构建工具,能够 显著提升 前端开发体验
为什么能够显著提升开发体验呢?首先我们了解结构建工具做了哪些工作。
1. 传统构建工具所做的工作(自动化)
- 模块化开发支持:支持直接从 node_modules 引入代码,支持多重模块化
- 处理代码的兼容性:比如 ES6 的代码降级,jsx转换为js, less/sass 转换为 css(不是构建工具做的,构建工具将这些工具集成进来自动化处理)
- 提高项目性能:压缩代码,代码分割
- 提高开发体验:提供开发服务器,能够解决服务跨域的问题(本地代理)。监听文件的变化,文件变化后能够自动调用相应的工具重新处理、打包,在浏览器重新运行(热更新)
这样,我们就不用管理代码如何处理,如何在浏览器运行,只需要关注开发工作即可。
目前的构建工具,通常是这个流程:从入口构建依赖图 => 对所有模块打包 => 浏览器运行。如下图:
当我们开始构建越来越大型的应用时,需要处理的 JavaScript 代码量也呈指数级增长。包含数千个模块的大型项目相当普遍。基于 JavaScript 开发的工具就会开始遇到性能瓶颈:通常需要很长时间(甚至是几分钟!)才能启动开发服务器,即使使用模块热替换(HMR),文件修改后的效果也需要几秒钟才能在浏览器中反映出来。如此循环往复,迟钝的反馈会极大地影响开发者的开发效率和幸福感。
Vite 旨在利用生态系统中的新进展解决上述问题:浏览器开始原生支持 ES 模块,且越来越多 JavaScript 工具使用编译型语言编写。
2.vite 的设计理念
2.1 开发服务器
Vite 通过在一开始将应用中的模块区分为 依赖 和 源码 两类,改进了开发服务器启动时间。
- 依赖 大多为在开发时不会变动的纯 JavaScript。一些较大的依赖(例如有上百个模块的组件库)处理的代价也很高。依赖也通常会存在多种模块化格式(例如 ESM 或者 CommonJS)。Vite 将会使用 esbuild 预构建依赖。esbuild 使用 Go 编写,并且比以 JavaScript 编写的打包器预构建依赖快 10-100 倍。
- 源码 通常包含一些并非直接是 JavaScript 的文件,需要转换(例如 JSX,CSS 或者 Vue/Svelte 组件),时常会被编辑。同时,并不是所有的源码都需要同时被加载(例如基于路由拆分的代码模块)。
Vite 以 原生 ESM 方式提供源码。这实际上是让浏览器接管了打包程序的部分工作:Vite 只需要在浏览器请求源码时进行转换并按需提供源码。根据情景动态导入代码,即只在当前屏幕上实际使用时才会被处理。如下图:
2.2 热更新(HMR)
传统的 HMR:当我们对代码做修改并保存后,webpack 会对修改的代码块以及该模块的依赖重新编译打包,并将新的模块发送至浏览器端,浏览器用新的模块代替旧的模块,从而实现了在不刷新浏览器的前提下更新页面。相比起直接刷新页面的方案,HMR 的优点是可以保存应用的状态。当然,随着项目体积的增长,热更新的速度也会随之下降。
在 Vite 中,HMR 是在原生 ESM 上执行的。当编辑一个文件时,Vite 只需要精确地使已编辑的模块与其最近的 HMR 边界之间的链失活(大多数时候只是模块本身),使得无论应用大小如何,HMR 始终能保持快速更新。
Vite 同时利用 HTTP 头来加速整个页面的重新加载(再次让浏览器为我们做更多事情):源码模块的请求会根据 304 Not Modified
进行协商缓存,而依赖模块请求则会通过 Cache-Control: max-age=31536000,immutable
进行强缓存,因此一旦被缓存它们将不需要再次请求。
2.3 为什么生产环境仍需打包
尽管原生 ESM 现在得到了广泛支持,但由于嵌套导入会导致额外的网络往返,在生产环境中发布未打包的 ESM 仍然效率低下(即使使用 HTTP/2)。为了在生产环境中获得最佳的加载性能,最好还是将代码进行 tree-shaking、懒加载和 chunk 分割(以获得更好的缓存)。
依赖预构建
1. 为什么需要依赖预构建
我们看这样一个例子。创建下面的目录,并在 index.html
中以 module 的形式引入 main.js
。
prebuild-demo
├─ index.html
├─ main.js
└─ package.json
// index.html
// ...
<script type="module" src="./main.js"></script>
// ...
接着,我们cnpm install lodash -S
安装 lodash
,并在 main.js
中写入:
// main.js
import { throttle } from 'lodash';
console.log(throttle);
然后,在浏览器打开 index.html
,会报错:
这是因为 ESModule 中,相对引用要采用
/
, ./
, 或 ../
开头。因此,不能够通过依赖的方式直接引入。依赖预构建,能够重写这部分模块引入,从而解决问题:
我们安装 vite,并用 vite 启动项目:
cnpm install vite && npx vite
打开控制台,throttle
已经能打印出来了。
再看 main.js ,模块引入变成了具体的地址:
除了依赖补全,依赖预构建还做了这两个工作:
- CommonJS 和 UMD 兼容性: 开发阶段中,Vite 的开发服务器将所有代码视为原生 ES 模块。因此,Vite 必须先将作为 CommonJS 或 UMD 发布的依赖项转换为 ESM。
- 提高性能: Vite 将有许多内部模块的 ESM 依赖关系转换为单个模块,以提高后续页面加载性能。比如官网的例子,lodash-es。
2. 缓存
2.1 文件系统缓存
Vite 会将预构建的依赖缓存到 node_modules/.vite
。它根据几个源来决定是否需要重新运行预构建步骤:
-
package.json
中的 dependencies 列表 - 包管理器的 lockfile,例如
package-lock.json
,yarn.lock
,或者pnpm-lock.yaml
- 可能在
vite.config.js
相关字段中配置过的
只有在上述其中一项发生更改时,才需要重新运行预构建。
如果出于某些原因,你想要强制 Vite 重新构建依赖,你可以用--force
命令行选项启动开发服务器,或者手动删除node_modules/.vite
目录。
2.2 浏览器缓存
解析后的依赖请求会以 HTTP 头 max-age=31536000,immutable
强缓存,以提高在开发时的页面重载性能。一旦被缓存,这些请求将永远不会再到达开发服务器。如果安装了不同的版本(这反映在包管理器的 lockfile 中),则附加的版本 query 会自动使它们失效。如果你想通过本地编辑来调试依赖项,你可以:
- 通过浏览器调试工具的 Network 选项卡暂时禁用缓存;
- 重启 Vite dev server,并添加
--force
命令以重新构建依赖; - 重新载入页面。
常用功能与配置
1. CSS Modules
任何以 .module.css 为后缀名的 CSS 文件都被认为是一个 CSS modules 文件。导入这样的文件会返回一个相应的模块对象。
也就是说我们直接可以模块引入:
import styles from "./index.module.less";```
CSS Modules 比较常用的配置:
// ...
css: {
modules: {
generateScopedName: "[path][name]__[local]__[hash:5]",
localsConvention: "camelCaseOnly"
}
},
// ...
其中,
-
generateScopedName: 生成的类名格式
-
localsConvention:修改生成对象的 key 的展示形式 (驼峰还是中划线)
2. CSS 预处理器
vite 提供了对 sass/less/stylus 的内置支持。
我们只需要安装相应的预处理器依赖即可直接使用。
cnpm install less -D
比较常用的配置:
css: {
// ...
preprocessorOptions: {
less: {
additionalData: `@import '@/assets/styles/common.less';`, // 全局注入样式文件
modifyVars: {
'primary-color': '#409eff' // 全局样式变量
},
javascriptEnabled: true
}
},
devSourcemap: true // 默认false,设为true开发阶段启动用sourcemap
},
3. PostCSS
vite 提供了对 PostCSS 的内置支持。
我们可以在 vite.config.js
中配置 PostCSS ,也可以直接新建 postcss.config.js
文件配置 PostCSS 。
这里我们使用 postcss-preset-env 试一下,postcss-preset-env 包含一系列 PostCSS 的插件。比如浏览器前缀自动添加:
cnpm install postcss-preset-env -D
根目录新增 postcss.config.js
,并配置如下:
import postcssPresetEnv from 'postcss-preset-env';
export default {
plugins: [postcssPresetEnv()]
};
重启开发服务器,就能够看到浏览器前缀自动添加了:
4. 静态资源处理
4.1 资源引入
vite 中,引入一个静态资源会返回解析后的公共路径:
import exampleImg from "/src/assets/example.png";
exampleImg
在开发时会是 /src/assets/example.png
,生产环境会是 /assets/example.2d8efhg.png
。类似于 webpack4 中的 file-loader
.
- 常见的图像、媒体和字体文件类型被自动检测为资源。你可以使用
assetsInclude
选项 扩展内部列表。
export default defineConfig({
assetsInclude: ['**/*.gltf'] // 会把.gltf文件当做资源文件处理
})
- 较小的资源体积小于
assetsInlineLimit
选项值 则会被内联为 base64 data URL。
// ...
build: {
assetsInlineLimit: 8 * 1024, // 小于 8 KB的资源会被内联成base64格式
// ...
},
也可以通过 ?raw
后缀声明作为字符串引入。类似于 webpack4 中的 raw-loader
。
import helloString from "./test.txt?raw";
console.log("exampleImg", helloString); // hello, vite
4.2 public 目录
如果你有下列这些资源:
- 不会被源码引用(例如 robots.txt)
- 必须保持原有文件名(没有经过 hash)
- ...或者你压根不想引入该资源,只是想得到其 URL。
那么你可以将该资源放在指定的 public 目录中,它应位于你的项目根目录。该目录中的资源在开发时能直接通过 / 根路径访问到,并且打包时会被完整复制到目标目录的根目录下。
目录默认是 /public,但可以通过 publicDir 选项 来配置。
请注意: - 引入 public 中的资源永远应该使用根绝对路径 —— 举个例子,
public/icon.png
应该在源码中被引用为/icon.png
。 - public 中的资源不应该被 JavaScript 文件引用。
5. alias 与 extensions
通过下面的代码配置别名和扩展名缩写:
import { resolve } from "path";
// ...
resolve: {
alias: {
"@": resolve(__dirname, "./src")
},
extensions: [".jsx", ".js", ".tsx", ".ts", ".json"]
},
6. 本地开发服务器
server: {
open: true, // 自动打开浏览器
host: "0.0.0.0",
port: 9999,
strictPort: true, // 设置为false,端口被占用会直接退出
proxy: {
"/webapi": {
target: "http://10.2.2.98:8090",
changeOrigin: true,
rewrite: (path) => path.replace(/^\/webapi/, "")
}
}
}
环境变量
1. 内建环境变量
Vite 在一个特殊的 import.meta.env
对象上暴露环境变量。这里有一些在所有情况下都可以使用的内建变量:
-
import.meta.env.MODE
: {string} 应用运行的模式。 -
import.meta.env.BASE_URL
: {string} 部署应用时的基本 URL。他由 base 配置项决定。 -
import.meta.env.PROD
: {boolean} 应用是否运行在生产环境。 -
import.meta.env.DEV
: {boolean} 应用是否运行在开发环境 (永远与import.meta.env.PROD
相反)。 -
import.meta.env.SSR
: {boolean} 应用是否运行在 server 上。
2. .env
文件
我们可以在 .env
文件中编写自己需要的环境变量。
.env # 所有情况下都会加载
.env.local # 所有情况下都会加载,但会被 git 忽略
.env.[mode] # 只在指定模式下加载
.env.[mode].local # 只在指定模式下加载,但会被 git 忽略
自己编写的环境变量必须以 VITE_
为前缀,比如:
VITE_SOME_KEY=123
DB_PASSWORD=foobar // 不合法
console.log(import.meta.env.VITE_SOME_KEY) // 123
console.log(import.meta.env.DB_PASSWORD) // undefined
3. 模式
默认情况下,开发服务器 (dev 命令) 运行在 development (开发) 模式,而 build 命令则运行在 production (生产) 模式。
如果我们需要额外的模式,则可以使用 --mode 覆盖默认的模式:
vite build --mode staging
同时我们还需要一个 .env.staging
文件来定义环境变量:
# .env.staging
NODE_ENV=production
VITE_HTTP=http://10.2.2.245:8890
生产构建优化
1. 分包策略
生产环境打包的时候,我们可能会需要分包。比如:把依赖单独打一个包,这样就可以避免依赖被重复打包。
build: {
assetsInlineLimit: 8 * 1024, // 小于8KB的资源base64内联
rollupOptions: {
output: {
manualChunks(id) {
if (id.includes("node_modules")) {
return "vendor";
}
}
}
}
},
rollupOptions
里还能够配置打包生成的目录,一个常用的配置:
build: {
rollupOptions: {
output: {
// ...
assetFileNames: (assetInfo) => {
var info = assetInfo.name.split(".");
var extType = info[info.length - 1];
if (/\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/i.test(assetInfo.name)) {
extType = "media";
} else if (/\.(png|jpe?g|gif|svg)(\?.*)?$/.test(assetInfo.name)) {
extType = "img";
} else if (/\.(woff2?|eot|ttf|otf)(\?.*)?$/i.test(assetInfo.name)) {
extType = "fonts";
}
return `static/${extType}/[name]-[hash][extname]`;
},
chunkFileNames: "static/js/[name]-[hash].js",
entryFileNames: "static/js/[name]-[hash].js"
}
}
},
打包后的目录:
2. 动态导入
动态导入(import 函数)是 ES6 的新特性。使用动态导入语法能够实现分包,进而实现懒加载。通常用于路由的懒加载。下面是一个例子。
// import { throttle } from 'lodash';
// console.log('object :>> throttle', throttle);
import('lodash').then(({ throttle }) => {
console.log('object :>> throttle', throttle);
});
上面是直接导入,下面是动态导入。二者打包结果如下:
可以看出,下面的 lodash 已经自动分包了。
3. 图片压缩、gzip 压缩
通过 vite-plugin-imagemin
,vite-plugin-compression
插件可以实现图片压缩与 gzip 压缩。用法也比较简单:
import compression from 'vite-plugin-compression';
import imagemin from 'vite-plugin-imagemin';
export default defineConfig({
// ...
plugins: [react(), compression(), imagemin()]
// ...
});
4. CDN 优化(外网环境)
通过 vite-plugin-cdn-import
插件能够将一些依赖使用 cdn 加载,从而降低包的大小,加快依赖加载速度。用法如下:
import { Plugin as importToCDN } from 'vite-plugin-cdn-import';
export default defineConfig({
plugins: [
react(),
importToCDN({
modules: [
{
name: 'lodash',
var: '_',
path: 'https://cdn.bootcdn.net/ajax/libs/lodash.js/4.17.21/lodash.min.js'
}
]
})
]
});
参考文档
Vite 官方文档: https://cn.vitejs.dev/guide/
Vite 和 webpack、rollup 打包工具对比:https://blog.csdn.net/Ambibibition/article/details/127766551
Vite世界指南(带你从0到1深入学习 vite):
https://www.bilibili.com/video/BV1GN4y1M7P5/?spm_id_from=333.999.0.0&vd_source=41e6d1d28e504860272fd13300cb250c