Vite 深度剖析(二)
- [7. CSS 工程化处理](#7. CSS 工程化处理)
-
- [7.1 全局引入 css](#7.1 全局引入 css)
- [7.2 下载 scss](#7.2 下载 scss)
- [7.3 全局引入 scss 变量](#7.3 全局引入 scss 变量)
- [7.4 局部引入 css 类](#7.4 局部引入 css 类)
- [7.5 修改局部引入的类名](#7.5 修改局部引入的类名)
- [7.6 postcss(CSS 后处理器)](#7.6 postcss(CSS 后处理器))
-
- [7.6.1 下载](#7.6.1 下载)
- [7.6.2 postcss.config.cjs 配置(不支持热更新)](#7.6.2 postcss.config.cjs 配置(不支持热更新))
- [7.6.3 vite.cofing.ts 中配置(支持热更新,推荐)](#7.6.3 vite.cofing.ts 中配置(支持热更新,推荐))
- [7.6.4 package.json 中配置(没生效)](#7.6.4 package.json 中配置(没生效))
- [7.7 CSS in JS 方案(不好用)](#7.7 CSS in JS 方案(不好用))
-
- [7.7.1 未使用前](#7.7.1 未使用前)
- [7.7.2 使用 styled-components](#7.7.2 使用 styled-components)
- [7.8 tailwindcss(有一定的学习成本)](#7.8 tailwindcss(有一定的学习成本))
-
- [7.8.1 下载和使用](#7.8.1 下载和使用)
- [7.8.2 智能提示](#7.8.2 智能提示)
- [8. 静态资源处理](#8. 静态资源处理)
-
- [8.1 别名设置](#8.1 别名设置)
- [8.2 默认静态资源后缀](#8.2 默认静态资源后缀)
- [8.3 静态资源引入](#8.3 静态资源引入)
-
- [8.3.1 SFC 模板中引入](#8.3.1 SFC 模板中引入)
- [8.3.2 url()方式 在CSS中引入](#8.3.2 url()方式 在CSS中引入)
- [8.3.3 import 方式导入](#8.3.3 import 方式导入)
- [8.3.4 使用import动态导入的方式(打包会产生额外的js文件,不推荐)](#8.3.4 使用import动态导入的方式(打包会产生额外的js文件,不推荐))
- [8.3.5 使用new URL的方式处理动态路径(推荐)](#8.3.5 使用new URL的方式处理动态路径(推荐))
- [8.4 import.meta.glob 导入多个模块(图片、路由等)](#8.4 import.meta.glob 导入多个模块(图片、路由等))
- [8.5 引入外部资源文件(比如图片CDN)](#8.5 引入外部资源文件(比如图片CDN))
- [8.6 未被列入静态资源文件处理](#8.6 未被列入静态资源文件处理)
-
- [8.6.1 作为静态资源处理(方式一)](#8.6.1 作为静态资源处理(方式一))
- [8.6.2 显式 URL 引入 xxx?url(方式二)](#8.6.2 显式 URL 引入 xxx?url(方式二))
- [8.6.3 将资源内容引入为字符串 xxx?raw(方式三)](#8.6.3 将资源内容引入为字符串 xxx?raw(方式三))
- [8.7 public 目录下的资源](#8.7 public 目录下的资源)
-
- [8.7.1 使用方式](#8.7.1 使用方式)
- [8.7.2 适合放在public的文件](#8.7.2 适合放在public的文件)
- [8.8 静态资源的两种构建方式](#8.8 静态资源的两种构建方式)
-
- [8.8.1 单文件和base64](#8.8.1 单文件和base64)
- [8.8.2 修改静态资源构建方式阈值(build.assetsInlineLimit)](#8.8.2 修改静态资源构建方式阈值(build.assetsInlineLimit))
- [9. HMR 模块热替换](#9. HMR 模块热替换)
-
- [9.1 vue 和 react 自带模块热替换](#9.1 vue 和 react 自带模块热替换)
- [9.2 vite 项目默认未实现模块热替换](#9.2 vite 项目默认未实现模块热替换)
- [9.2 实现模块热替换](#9.2 实现模块热替换)
-
- [9.2.1 前期准备](#9.2.1 前期准备)
- [9.2.2 实现自身模块的更新](#9.2.2 实现自身模块的更新)
- [9.2.3 指定子模块热更新](#9.2.3 指定子模块热更新)
- [9.2.4 hot.dispose 模块销毁时逻辑](#9.2.4 hot.dispose 模块销毁时逻辑)
- [9.2.5 hot.data 共享数据(实现模块数据持久化,不受热更新影响)](#9.2.5 hot.data 共享数据(实现模块数据持久化,不受热更新影响))
- [10. Vite 插件机制](#10. Vite 插件机制)
-
- [10.1 服务启动阶段钩子(按顺序执行,通用2个,vite独有3个)](#10.1 服务启动阶段钩子(按顺序执行,通用2个,vite独有3个))
- [10.2 了解插件钩子函数(执行阶段和顺序测试)](#10.2 了解插件钩子函数(执行阶段和顺序测试))
- [10.3 编写一个打包计时插件](#10.3 编写一个打包计时插件)
- [10.4 修改html的内容(transformIndexHtml)](#10.4 修改html的内容(transformIndexHtml))
- [10.5 传入模块时调用的钩子](#10.5 传入模块时调用的钩子)
-
- [10.5.1 将插件作为虚拟模块暴露(比如默认暴露一个函数)](#10.5.1 将插件作为虚拟模块暴露(比如默认暴露一个函数))
- [10.5.2 结合策略模式,暴露多个虚拟模块函数](#10.5.2 结合策略模式,暴露多个虚拟模块函数)
7. CSS 工程化处理
原生CSS的问题:
- 开发体验欠佳
- 样式污染问题
- 浏览器兼容问题
- 代码体积问题
工程化方案
7.1 全局引入 css
(1)创建src/style.css:
typescript
*,
*::before,
*::after {
box-sizing: border-box;
}
body {
margin: 0;
display: flex;
place-items: center;
min-width: 320px;
min-height: 100vh;
color: rgba(255, 255, 255);
background-color: #242424;
}
html {
font-family: sans-serif;
line-height: 1.15;
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
-ms-overflow-style: scrollbar;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
#app {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
text-align: center;
}
(2)在 src/main.ts中引入:
typescript
import { createApp } from "vue";
import App from "./App.vue";
import "./style.css";
createApp(App).mount("#app");
7.2 下载 scss
typescript
pnpm add sass -D
7.3 全局引入 scss 变量
(1)创建src/styles/var.scss:
typescript
$pct33:33%;
$pct67:67%;
$pct100:100%;
(2)在 vite.config.ts 中引入,关键代码:
typescript
css: {
preprocessorOptions: {
scss: {
//注意最后要加上分号;
additionalData: '@use "@/styles/var.scss" as *;',
},
},
},
resolve: {
alias: {
"@": path.resolve("./src"),
},
},
这里需要使用 as *,否则后续使用时就需要加上变量的文件名,比如 var.$pct33。
完整代码:
typescript
// defineConfig 用于自动提示配置项
import { defineConfig, ConfigEnv, UserConfig, loadEnv } from "vite";
import { wrapperEnv } from "./build/getEnv";
import vue from "@vitejs/plugin-vue";
import path from "path";
export default defineConfig(({ command, mode }: ConfigEnv): UserConfig => {
// config 是默认配置对象,有command、mode等属性
// 执行npm run build或者npm run test 时,可以看到对应的,命令和模式
// console.log(command);
// console.log(mode);
const root = process.cwd();
// 这样只读取以VITE_开头的环境变量
const env = loadEnv(mode, root, "VITE_");
// console.log(env);
const viteEnv = wrapperEnv(env);
console.log(viteEnv);
// 可以根据不同的命令和模式,返回不同的配置对象
return {
root,
plugins: [vue()],
server: {
port: viteEnv.VITE_PORT,
open: viteEnv.VITE_OPEN,
},
// esbuild 已弃用,看官网使用build配置
// esbuild: {
// pure: viteEnv.VITE_DROP_CONSOLE ? ["console.log", "debugger"] : [],
// },
build: {
// 使用该选项需要 pnpm add -D terser
// 参考 https://cn.vitejs.dev/config/build-options#build-minify
minify: "terser",
terserOptions: {
compress: {
drop_console: viteEnv.VITE_DROP_CONSOLE,
drop_debugger: viteEnv.VITE_DROP_CONSOLE,
},
},
},
// optimizeDeps: {
// exclude: ["lodash-es"],
// },
css: {
preprocessorOptions: {
scss: {
//注意最后要加上分号;
additionalData: '@use "@/styles/var.scss" as *;',
},
},
},
resolve: {
alias: {
"@": path.resolve("./src"),
},
},
};
});
(3)创建 src/views/NotFound.vue(使用全局引入的css变量,前面加$):
typescript
<template>
<div title="404">404</div>
</template>
<script setup lang="ts">
</script>
<style scoped lang="scss">
div {
display: flex;
color: #fff;
font-size: 96px;
font-family: "Fira Mono", monospace;
letter-spacing: -7px;
animation: glitch 1s linear infinite;
&::before,
&::after {
content: attr(title);
position: absolute;
left: 0;
}
&::before {
animation: glitchTop 1s linear infinite;
clip-path: polygon(0 0, $pct100 0, $pct100 $pct33, 0 $pct33);
}
&::after {
animation: glitchBottom 1.5s linear infinite;
clip-path: polygon(0 $pct67, $pct100 $pct67, $pct100 $pct100, 0 $pct100);
}
}
@keyframes glitch {
2%,
64% {
transform: translate(2px, 0) skew(0deg);
}
4%,
60% {
transform: translate(-2px, 0) skew(0deg);
}
62% {
transform: translate(0, 0) skew(5deg);
}
}
@keyframes glitchTop {
2%,
64% {
transform: translate(2px, -2px);
}
4%,
60% {
transform: translate(-2px, 2px);
}
62% {
transform: translate(13px, -1px) skew(-13deg);
}
}
@keyframes glitchBottom {
2%,
64% {
transform: translate(-2px, 0);
}
4%,
60% {
transform: translate(-2px, 0);
}
62% {
transform: translate(-22px, 5px) skew(21deg);
}
}
</style>
(4)在 src/App.vue中使用:
typescript
<template>
<div>
<!-- <h2>Welcome!!!</h2>
<input type="text" @input="handleInput" />
<button @click="handleCounter">count is {{ count }}</button> -->
<NotFound />
</div>
</template>
<script lang="ts" setup>
// import { ref } from "vue";
// import { debounce } from "lodash-es";
// const count = ref(0);
// const handleCounter = () => {
// count.value++;
// };
// const handleInput = debounce((e: Event) => {
// console.log((e.target as HTMLInputElement).value);
// }, 1000);
import NotFound from "./views/NotFound.vue";
</script>
<style lang="scss" scoped></style>

7.4 局部引入 css 类
(1)src/vite-env.d.ts 中加入文件引入相关的类型说明,关键代码:
typescript
/// <reference types="vite/client" />
否则在引入样式文件时会有引入报错提示。

(2)创建src/styles/content.module.scss:
typescript
.text{
font-family: 'Courier New', Courier, monospace;
font-size: 20px;
color: #eee;
}
(3)src/views/NotFound.vue 中引入,关键代码:
typescript
<template>
<div title="404">404</div>
<p :class="$content.text"">
Page Not Found
</p>
</template>
<script setup lang="ts">
import $content from "../styles/content.module.scss";
</script>
7.5 修改局部引入的类名
(1)vite.config.ts 关键代码:
typescript
css: {
modules: {
// name 表示当前文件名,local 表示当前类名,hash:base64:5 表示生成一个长度为5的base64编码的hash值
generateScopedName: "[name]__[local]___[hash:base64:5]",
},
},

7.6 postcss(CSS 后处理器)
官网:https://postcss.docschina.org/doc/api.html

7.6.1 下载
typescript
pnpm add postcss -D
pnpm add postcss-preset-env -D
7.6.2 postcss.config.cjs 配置(不支持热更新)
创建postcss.config.cjs:
typescript
const presetEnv = require("postcss-preset-env");
module.exports = {
plugins: [
presetEnv({
browsers: ["last 2 versions", "> 1%", "IE 11"],
autoprefixer: { grid: true },
}),
],
};
(3)运行 pnpm dev
可以看到,针对对应浏览器


7.6.3 vite.cofing.ts 中配置(支持热更新,推荐)
(1)配置
同样,可以在 vite.cofing.ts 中进行postcss的配置(记得将 postcss.config.cjs 中的代码注释,避免影响)
关键代码:
typescript
import presetEnv from "postcss-preset-env";
postcss: {
plugins: [
presetEnv({
browsers: ["last 2 versions", "> 1%", "IE 11"],
autoprefixer: { grid: true },
}),
],
},
完整配置代码:
typescript
// defineConfig 用于自动提示配置项
import { defineConfig, ConfigEnv, UserConfig, loadEnv } from "vite";
import { wrapperEnv } from "./build/getEnv";
import vue from "@vitejs/plugin-vue";
import path from "path";
import presetEnv from "postcss-preset-env";
export default defineConfig(({ command, mode }: ConfigEnv): UserConfig => {
// config 是默认配置对象,有command、mode等属性
// 执行npm run build或者npm run test 时,可以看到对应的,命令和模式
// console.log(command);
// console.log(mode);
const root = process.cwd();
// 这样只读取以VITE_开头的环境变量
const env = loadEnv(mode, root, "VITE_");
// console.log(env);
const viteEnv = wrapperEnv(env);
console.log(viteEnv);
// 可以根据不同的命令和模式,返回不同的配置对象
return {
root,
plugins: [vue()],
server: {
port: viteEnv.VITE_PORT,
open: viteEnv.VITE_OPEN,
},
// esbuild 已弃用,看官网使用build配置
// esbuild: {
// pure: viteEnv.VITE_DROP_CONSOLE ? ["console.log", "debugger"] : [],
// },
build: {
// 使用该选项需要 pnpm add -D terser
// 参考 https://cn.vitejs.dev/config/build-options#build-minify
minify: "terser",
terserOptions: {
compress: {
drop_console: viteEnv.VITE_DROP_CONSOLE,
drop_debugger: viteEnv.VITE_DROP_CONSOLE,
},
},
},
// optimizeDeps: {
// exclude: ["lodash-es"],
// },
css: {
preprocessorOptions: {
scss: {
//注意最后要加上分号;
additionalData: '@use "@/styles/var.scss" as *;',
},
},
modules: {
// name 表示当前文件名,local 表示当前类名,hash:base64:5 表示生成一个长度为5的base64编码的hash值
generateScopedName: "[name]__[local]___[hash:base64:5]",
},
postcss: {
plugins: [
presetEnv({
browsers: ["last 2 versions", "> 1%", "IE 11"],
autoprefixer: { grid: true },
}),
],
},
},
resolve: {
alias: {
"@": path.resolve("./src"),
},
},
};
});
(2)运行pnpm dev
现在注释vite.config.ts中的相关配置,就会发现浏览器对应的css代码会实时变化,而不需要进行重启。
7.6.4 package.json 中配置(没生效)
关键代码:
typescript
"browserslist": {
"production": [
"last 4 version",
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 2 version",
"> 1%",
"IE 11"
]
}
可以区分不同环境的处理方式。不过经过测试,我发现在package.json中的配置并没有效果,所以还是建议在vite.config.ts中进行配置。
7.7 CSS in JS 方案(不好用)

对于 vue 而言,本身就已经足够好用,我们使用react项目来看看。
7.7.1 未使用前
打开之前的 vite-react-demo 项目,改造App.tsx:
typescript
import React, { useState } from 'react';
const App = () => {
const [count, setCount] = useState(0)
const [hovered, setHovered] = useState(false)
const buttonStyle = {
padding: '8px 16px',
border: hovered ? '1px solid transparent' : '1px solid #ccc',
borderRadius: '4px',
cursor: 'pointer',
backgroundColor: hovered ? '#e0e0e0' : '#fff',
color: '#333',
fontSize: '16px',
transition: 'all 0.3s ease-in-out'
}
return (
<div>
<h2>Hello World!!!</h2>
<button
style={buttonStyle}
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
onClick={
() => {
setCount(count + 1);
}
}>count is { count }</button>
</div>
)
}
export default App;

7.7.2 使用 styled-components
(1)下载依赖
typescript
pnpm add styled-components -D
pnpm add babel-plugin-styled-components -D
(2)创建babel.config.js:
typescript
export default {
plugins: ["babel-plugin-styled-components"],
};
(3)在App.tsx中使用:
typescript
import React, { useState } from 'react';
import styled from 'styled-components';
// 定义一个按钮样式组件
const StyledButton = styled.button`
padding: 8px 16px;
border: 1px solid #ccc;
border-radius: 4px;
cursor: pointer;
background-color: #fff;
outline: none;
color: #333;
font-size: 16px;
transition: all 0.3s ease-in-out;
&:hover {
background-color: #eee;
}
`
// 在StyledButton的基础上,定义一个小按钮组件
const StyledSmallButton = styled(StyledButton)`
padding: 4px 8px;
font-size: 12px;
`
const App = () => {
const [count, setCount] = useState(0)
const [hovered, setHovered] = useState(false)
const buttonStyle = {
padding: '8px 16px',
border: hovered ? '1px solid transparent' : '1px solid #ccc',
borderRadius: '4px',
cursor: 'pointer',
backgroundColor: hovered ? '#e0e0e0' : '#fff',
color: '#333',
fontSize: '16px',
transition: 'all 0.3s ease-in-out'
}
return (
<div>
<h2>Hello World!!!</h2>
{/* 直接使用style对象 */}
<button
style={buttonStyle}
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
onClick={
() => {
setCount(count + 1);
}
}>count is { count }</button>
<br />
<br />
{/* 使用styled-components */}
<StyledButton
onClick={
() => {
setCount(count + 1);
}
}>count is { count }</StyledButton>
<StyledSmallButton
onClick={
() => {
setCount(count + 1);
}
}>count is { count }</StyledSmallButton>
</div>
)
}
export default App;
(4)测试

相对于react本身的hover样式书写,使用styled-components会相对简单些,但是还是感觉有些难受。
7.8 tailwindcss(有一定的学习成本)
7.8.1 下载和使用
中文官网:https://tailwind.nodejs.cn/docs/installation/using-vite

(1)下载
typescript
npm install tailwindcss @tailwindcss/vite -D
(2)vite.config.ts 添加配置,关键代码:
typescript
import { defineConfig } from 'vite'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
plugins: [
tailwindcss(),
],
})
(3)创建src/tailwind.css:
typescript
@import "tailwindcss";
(4)在 src/main.tsx 中引入
typescript
import React from 'react'
import ReactDOM from 'react-dom/client'
import './tailwind.css'
import App from './App.tsx'
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>
)
(5)在 App.tsx 中使用,关键代码:
typescript
<header>
<p className='bg-red-100'>tailwind 样式</p>
</header>
7.8.2 智能提示
(1)VSCode 下载 Tailwind CSS IntelliSence 插件
(2)在用户设置(User.JSON),添加
typescript
"editor.quickSuggestions": {
"strings": "on"
}
然后再写tailwindcss代码,就会有提示了
8. 静态资源处理
8.1 别名设置
(1)vite.config.ts 设置别名,关键代码:
typescript
import { defineConfig } from 'vite';
import path from 'path';
export default defineConfig({
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),
}
}
});
(2)tsconfig.json 关键代码:
typescript
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@components/*": ["src/components/*"]
}
}
}
(3)App.vue 中使用别名@引入文件:
typescript
<template>
<div>
<input type="text" @input="handleInput" />
<button @click="handleCounter">count is {{ count }}</button> -->
<NotFound />
</div>
</template>
<script lang="ts" setup>
import NotFound from "@/views/NotFound.vue";
</script>
<style lang="scss" scoped></style>
8.2 默认静态资源后缀

8.3 静态资源引入
(1)创建 src/assets 文件夹,放入资源文件

(2)创建 ImageShow.vue (用于展示资源文件):
typescript
<template>
<div class="season">
<button class="btn-primary" @click="handleChange" value="spring">春</button>
<button class="btn-primary" @click="handleChange" value="summer">夏</button>
<button class="btn-primary" @click="handleChange" value="autumn">秋</button>
<button class="btn-primary" @click="handleChange" value="winter">冬</button>
</div>
<div class="card">
<img src="@/assets/spring.jpg" alt="" />
</div>
</template>
<script lang="ts" setup>
const handleChange = (e: Event) => {
console.log((e.target as HTMLButtonElement).value);
};
</script>
<style scoped lang="scss">
.season {
padding-top: 30px;
/* height: 100vh;
background-image: url(../assets/spring.jpg); */
background-size: cover;
background-position: center;
transition: background-image 0.3s;
}
.btn-primary {
background-color: #00a0e9;
border-color: #00a0e9;
color: #fff;
font-size: 16px;
padding: 10px 20px;
border-radius: 5px;
margin: 0 10px;
cursor: pointer;
outline: none;
border: none;
box-shadow: 0 0 5px rgba(0, 0, 0, 0.3);
&:hover {
background-color: #008cc4;
border-color: #008cc4;
}
&:focus {
background-color: #0077b3;
border-color: #0077b3;
}
}
.card {
display: inline-block;
margin: 16px;
width: 50%;
border-radius: 5px;
border: 1px solid #ddd;
box-shadow: 0 0 3rem -1rem rgba(0, 0, 0, 0.5);
transition: transform 0.3s;
img {
max-width: 100%;
object-fit: cover;
}
&:hover {
transform: translateY(-0.5rem) scale(1.0125);
box-shadow: 0 0.5em 3rem -1rem rgba(0, 0, 0, 0.5);
}
}
</style>
(3)在 App.vue 中引入:
typescript
<template>
<div>
<ImageShow />
</div>
</template>
<script lang="ts" setup>
import ImageShow from "@/views/ImageShow.vue";
</script>
<style lang="scss" scoped></style>
(4)修改下 style.css:
css
*,
*::before,
*::after {
box-sizing: border-box;
}
body {
margin: 0;
/* display: flex; */
/* place-items: center; */
min-width: 320px;
/* min-height: 100vh; */
color: rgba(255, 255, 255);
/* background-color: #242424; */
}
html {
font-family: sans-serif;
line-height: 1.15;
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
-ms-overflow-style: scrollbar;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
#app {
/* max-width: 1280px; */
/* margin: 0 auto; */
/* padding: 2rem; */
text-align: center;
}
(5)执行 pnpm dev
8.3.1 SFC 模板中引入
(1)关键代码:
typescript
<img src="@/assets/spring.jpg" alt="" />
(2)运行 pnpm dev

(3)运行 pnpm bp

(4)对比。

生产环境并没有src文件夹,直接生成assets文件夹及其资源(做了tree shaking)。并且每个资源文件都有hash码作为文件指纹,用于方便浏览器缓存。
8.3.2 url()方式 在CSS中引入
关键代码:
typescript
background-image: url(../assets/spring.jpg);
8.3.3 import 方式导入
关键代码:
typescript
<template>
<div class="card">
<img :src="spring" alt="" />
</div>
</template>
<script lang="ts" setup>
import spring from "@/assets/spring.jpg";
</script>
8.3.4 使用import动态导入的方式(打包会产生额外的js文件,不推荐)
关键代码:
typescript
<template>
<div class="season">
<button class="btn-primary" @click="handleChange" value="spring">春</button>
<button class="btn-primary" @click="handleChange" value="summer">夏</button>
<button class="btn-primary" @click="handleChange" value="autumn">秋</button>
<button class="btn-primary" @click="handleChange" value="winter">冬</button>
</div>
<div class="card">
<!-- 使用import动态导入的方式 -->
<img :src="imgPath" alt="" />
</div>
</template>
<script lang="ts" setup>
import { ref } from "vue";
// import spring from "@/assets/spring.jpg";
// 直接引入变量的方式是没有效果的,vite并不会帮我们去解析路径
// const spring = ref('/src/assets/spring.jpg');
import spring from "@/assets/spring.jpg";
const imgPath = ref(spring);
const handleChange = (e: Event) => {
const v = (e.target as HTMLButtonElement).value;
import(`@/assets/${v}.jpg`).then((res) => {
console.log(res);
imgPath.value = res.default;
});
};
// const handleChange = (e: Event) => {
// console.log((e.target as HTMLButtonElement).value);
// };
</script>
可以实现动态切换文件路径的效果,但是打包回产生额外的js文件,用于帮助引入资源文件。

8.3.5 使用new URL的方式处理动态路径(推荐)
关键代码:
typescript
<template>
<div class="season">
<button class="btn-primary" @click="handleChange" value="spring">春</button>
<button class="btn-primary" @click="handleChange" value="summer">夏</button>
<button class="btn-primary" @click="handleChange" value="autumn">秋</button>
<button class="btn-primary" @click="handleChange" value="winter">冬</button>
</div>
<div class="card">
<!-- 使用new URL的方式处理 -->
<img :src="url" alt="" />
</div>
</template>
<script lang="ts" setup>
import { ref, computed } from "vue";
// 使用new URL的方式处理变量的静态资源路径
const imgPath = ref("spring");
// 计算属性处理URL地址
const url = computed(() => {
const href = new URL(`../assets/${imgPath.value}.jpg`, import.meta.url).href;
console.log("🚀 ~ href:", href);
return href;
});
// 事件切换路径字符串
const handleChange = (e: Event) => {
const v = (e.target as HTMLButtonElement).value;
imgPath.value = v;
};
</script>



可以发现,无论是开发还是生产环境,用new URL生成的都是完整路径(和之前的不同),并且打包后并不会产生多余的js文件。
8.4 import.meta.glob 导入多个模块(图片、路由等)
参考:https://cn.vitejs.dev/guide/features#glob-import
typescript
<template>
<!-- 通过import.meta.glob显示多张图片 -->
<div class="card" v-for="(img, index) in imgUrls" :key="index">
<img :src="img" alt="" />
</div>
</template>
<script lang="ts" setup>
// 使用import.meta.glob 导入多个模块文件
// 这个api不仅仅是适用于静态资源的,更多的时候是处理js动态文件的,比如动态路由
const monthImgs = import.meta.glob("@/assets/month/*.jpg", { eager: true });
// 返回的是由键值对组成的对象
// key(string) value(module default)
// console.log(monthImgs)
const imgUrls = Object.values(monthImgs).map((mod) => {
return (mod as { default: string }).default;
});
console.log(imgUrls);
</script>

8.5 引入外部资源文件(比如图片CDN)
(1)添加环境变量
.env.development 等所有环境配置文件加上:
typescript
# 网络图片资源前缀 自定义
VITE_IMG_BASE_URL=https://cf-assets.www.cloudflare.com
(2)使用环境变量引入外部资源
ImageShow.vue 关键代码:
typescript
<img :src="baiduImg1" alt="" />
// 使用CDN图片资源
const baiduImg1 = new URL(
`../slt3lc6tev37/YiKHwui8iUBOpAWIAvvmX/5cbff68eb37d189e8a6f093223c4477f/access-control.png`,
import.meta.env.VITE_IMG_BASE_URL,
).href;
8.6 未被列入静态资源文件处理
8.6.1 作为静态资源处理(方式一)
(1)创建 src/assets/readme.md:
typescript
# readme
内容随意
(2)src/vite-env.d.ts 加入资源声明:
typescript
declare module "*.md" {
const str: string;
export default str;
}
(3)vite.config.ts 加入静态资源配置,关键代码:
typescript
assetsInclude: ["**/*.md"],
(4)App.vue 中引入,关键代码:
typescript
<script lang="ts" setup>
import md from "@/assets/readme.md";
console.log("🚀 ~ md:", md);
</script>

8.6.2 显式 URL 引入 xxx?url(方式二)
关键代码:
typescript
import md from "@/assets/readme.md?url";
8.6.3 将资源内容引入为字符串 xxx?raw(方式三)
关键代码:
typescript
import md from "@/assets/readme.md?raw";
console.log("🚀 ~ md:", md);

但是,其实还有更好玩的使用方式
typescript
<template>
<div>
<div v-html="rocketSvgRaw" class="svg-container"></div>
</div>
</template>
<script lang="ts" setup>
import rocketSvgRaw from "@/assets/rocket.svg?raw";
</script>
<style lang="scss">
.svg-container svg {
width: 100px;
height: 100px;
fill: #f00;
&:hover {
fill: #0f0;
}
}
</style>
悬浮svg填充颜色变化

8.7 public 目录下的资源
8.7.1 使用方式
(1)根目录创建public文件夹,放入资源vite.svg。
(2)在index.html引入图标:
html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<!-- public下的文件,直接引入 -->
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<title>Vite Vue Demo</title>
</head>
<body>
<div id="app"></div>
<!-- 在 index.html 中引入 src/index.ts // 直接支持ts文件,不需要配置webpack的loader。 -->
<!-- <script type="module" src="./src/index.ts"></script> -->
<script type="module" src="./src/main.ts"></script>
</body>
</html>


可以看到,开发环境和生产环境都能够正常获取资源。
8.7.2 适合放在public的文件

public 下的文件,在打包时会直接暴露在根路径下,并且不会生成文件指纹。所以适合放在public文件夹下的文件有以下几种:
(1)不会被源码引入的静态文件;
(2)必须保持原有文件名的文件;
(3)不想引入该资源,只是想得到其 URL;
8.8 静态资源的两种构建方式
8.8.1 单文件和base64
所有的静态资源都有两种构建方式:
- 当资源体积 >= 4KB时,会打包构建成单文件;
- 当资源体积 < 4KB 作为 base64 格式的字符串内联。
对于比较小的资源,适合内联到代码中,一方面对代码体积的影响很小,另一方面可以减少不必要的网络请求,优化网络性能。
注意:svg 格式的文件不受这个临时值的影响,始终会打包成单独的文件。
8.8.2 修改静态资源构建方式阈值(build.assetsInlineLimit)
(1)App.vue 引入 6.8KB 的 src/assets/logo.png 图片,关键代码:
typescript
<img src="@/assets/logo.png" alt="" />
运行 pnpm bp

此时是单文件格式。
(2)vite.config.ts 添加 build.assetsInlineLimit 配置,修改阈值为7KB(7168个字节)下打包为base64格式,关键代码:
typescript
build: {
assetsInlineLimit: 7168, // 7kb 以下的图片会被转换成 base64 格式,减少请求次数
},
重新 pnpm bp,之前的6.8KB(<7KB)的静态资源文件被打包成了base64格式。

9. HMR 模块热替换
HMR(Hot Module Replacement)模块热替换:在页面模块更新的时候,直接把页面中发生变化的模块替换为新的模块,同时不会影响其它模块的正常运作,类似于电脑U盘的热插拔。
HRM 解决了 模块局部更新 和 状态保存 两个问题。

一般来说,因为 vue 和 react 插件已经帮我们处理好了模块热更新的过程,所以我们基本不用更改相关代码。这里可以作为了解。
9.1 vue 和 react 自带模块热替换

开发环境,查看App.vue资源文件,就可以看到 import.meta.hot 等模块热替换api。在vite-react-demo项目的App.tsx 文件也可以看到。
不过,在生产环境不需要这个,所以会打包构建时会删除这部分代码。
9.2 vite 项目默认未实现模块热替换
node_modules/vite/types/hot.d.ts 文件声明了模块热替换的相关API类型。

打开 vite-demo 项目,pnpm dev。点击4次按钮:

修改 src/index.ts:
typescript
import { setupCounter } from "./counter.ts";
document.querySelector("#app")!.innerHTML = `
<div>
<h1>Hello Vite!</h1>
<button id="counter" type="button"></button>
</div>
`;
setupCounter(document.querySelector("#counter") as HTMLButtonElement);

发现并没有局部模块刷新(没有保留之前的状态),而是直接刷新了整个页面。

终端也可以看到相关提示。
9.2 实现模块热替换
9.2.1 前期准备
(1)添加src/render.ts:
typescript
export const render = () => {
const app = document.querySelector<HTMLDivElement>("#app")!;
app.innerHTML = `
<h2>hello Vite HMR</h2>
`;
};
(2)添加 src/state.ts:
typescript
let timer: NodeJS.Timeout | undefined;
export const initState = () => {
let count = 0;
timer = setInterval(() => {
let countElement = document.querySelector<HTMLDivElement>("#count")!;
countElement.innerHTML = `count: ${++count}`;
}, 1000);
};
注意,这里用了NoeeJS模块,需要在 vite-env.d.ts 中添加:
typescript
/// <reference types="vite/client" />
同时,还要在tsconfig.json中加上配置,关键代码:
json
{
"compilerOptions": {
"types": [
"node"
],
}
(3)添加 src/main.ts:
typescript
import { render } from "./render";
import { initState } from "./state";
render();
initState();
(4)修改 index.html:
html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<div id="app"></div>
<div id="count"></div>
<!-- 在 index.html 中引入 src/index.ts // 直接支持ts文件,不需要配置webpack的loader。 -->
<!-- <script type="module" src="./src/index.ts"></script> -->
<script type="module" src="./src/main.ts"></script>
</body>
</html>
(5)运行 pnpm dev

9.2.2 实现自身模块的更新
参考:https://vitejs.cn/vite6-cn/guide/api-hmr.html#hot-accept-cb
修改 src/render.ts,添加模块热更新代码:
typescript
export const render = () => {
// 实现自身模块的更新
if (import.meta.hot) {
import.meta.hot.accept((newModule) => {
console.log("~ newModule", newModule);
newModule.render();
});
}
const app = document.querySelector<HTMLDivElement>("#app")!;
app.innerHTML = `
<h2>hello Vite HMR</h2>
`;
};
此时,再修改,关键代码:
typescript
<h2>hello Vite HMR!!!</h2>
就会发现,其他模块的代码状态不会因为src/render.ts的修改而变化。这就实现了自身模块的热更新。

并且终端也会有相应的提示。

9.2.3 指定子模块热更新
参考:https://vitejs.cn/vite6-cn/guide/api-hmr.html#hot-accept-deps-cb
先注释 src/render.ts 中的热更新代码,避免影响。
typescript
/* 指定某个子模块的HMR */
// if (import.meta.hot) {
// import.meta.hot.accept("./render.ts", (newModule) => {
// newModule.render();
// });
// }
/* 指定多个子模块的HMR */
if (import.meta.hot) {
import.meta.hot.accept(["./render.ts", "./state.ts"], (modules) => {
const [renderModule, stateModule] = modules;
if (renderModule) {
renderModule.render();
}
if (stateModule) {
stateModule.initState();
}
});
}
这样就实现了一个或者多个子模块的热更新。
上述的代码还是有问题,当我们修改src/state.ts代码时,之前的 timer 并没有被销毁,所以每次修改都会产生一个新的定时器,从而造成数字的闪烁。
9.2.4 hot.dispose 模块销毁时逻辑
参考:https://vitejs.cn/vite6-cn/guide/api-hmr.html#hot-dispose-cb
一个接收自身的模块或一个期望被其他模块接收的模块可以使用 hot.dispose 来清除任何由其更新副本产生的持久副作用。
src/state.ts :
typescript
let timer: NodeJS.Timeout | undefined;
// 热更新时清除定时器,避免重复创建定时器
if (import.meta.hot) {
import.meta.hot.dispose(() => {
if (timer) {
clearInterval(timer);
}
});
}
export const initState = () => {
let count = 0;
timer = setInterval(() => {
let countElement = document.querySelector<HTMLDivElement>("#count")!;
countElement.innerHTML = `count: ${++count}`;
}, 1000);
};
刷新浏览器,修改 src/state.ts,发现确实不会有数字闪烁的问题,定时器确实被删除了。
但是又带来了一个新的问题:每次热更新,都会初始化 count 的值。
9.2.5 hot.data 共享数据(实现模块数据持久化,不受热更新影响)
参考:https://vitejs.cn/vite6-cn/guide/api-hmr.html#hot-data
import.meta.hot.data 对象在同一个更新模块的不同实例之间持久化。它可以用于将信息从模块的前一个版本传递到下一个版本。
修改 src/state.ts,将 count 使用 import.meta.hot.data.count 进行替换,从而保持状态:
typescript
let timer: NodeJS.Timeout | undefined;
// 热更新时清除定时器,避免重复创建定时器
if (import.meta.hot) {
//初始化count
if (!import.meta.hot.data.count) {
import.meta.hot.data.count = 0;
}
import.meta.hot.dispose(() => {
if (timer) {
clearInterval(timer);
}
});
}
export const initState = () => {
const getCount = () => {
const data = import.meta.hot.data || { count: 0 };
data.count = data.count + 1;
return data.count;
};
timer = setInterval(() => {
let countElement = document.querySelector<HTMLDivElement>("#count")!;
countElement.innerHTML = `count: ${getCount()}`;
}, 1000);
};
10. Vite 插件机制

10.1 服务启动阶段钩子(按顺序执行,通用2个,vite独有3个)
- config(vite独有钩子):在解析 Vite 配置前调用。钩子接收原始用户配置(命令行选项指定的会与配置文件合并)和一个描述配置环境的变量,包含正在使用的 mode 和 command。它可以返回一个将被深度合并到现有配置中的部分配置对象,或者直接改变配置(如果默认的合并不能达到预期的结果)。
注意,用户插件在运行这个钩子之前会被解析,因此在 config 钩子中注入其他插件不会有任何效果。
- configResolved(vite独有钩子):在解析 Vite 配置后调用。使用这个钩子读取和存储最终解析的配置。当插件需要根据运行的命令做一些不同的事情时,它也很有用。
注意,在开发环境下,command 的值为 serve(在 CLI 中,vite 和 vite dev 是 vite serve 的别名)。
-
options(通用钩子,下一个通用钩子是buildStart):这是构建阶段的第一个钩子。替换或操作传递给 rollup.rollup 的选项对象。返回 null 不会替换任何内容。如果只需要读取选项,则建议使用 buildStart 钩子,因为该钩子可以访问所有 options 钩子的转换考虑后的选项。
-
configureServer(vite独有钩子):是用于配置开发服务器的钩子。最常见的用例是在内部 connect 应用程序中添加自定义中间件。
-
buildStart(通用钩子):在每个 rollup.rollup 构建上调用。当你需要访问传递给 rollup.rollup() 的选项时,建议使用此钩子,因为它考虑了所有 options 钩子的转换,并且还包含未设置选项的正确默认值。
10.2 了解插件钩子函数(执行阶段和顺序测试)
(1)创建 src/plugins/vite-plugin-test.ts:
typescript
import { Plugin } from "vite";
export default function testPlugin(): Plugin {
return {
name: "vite-plugin-test",
// vite独有钩子函数,就是在读取最开始的配置信息
config(config, configEnv) {
console.log("🚀 ~ config:");
},
// vite独有钩子函数
configResolved(resolvedConfig) {
console.log("🚀 ~ resolvedConfig:");
},
// 通用钩子
options(opts) {
console.log("🚀 ~ options:");
},
// vite独有钩子,一般都是对开发服务器进行处理
configureServer(server) {
console.log("🚀 ~ configureServer:");
},
// 通用钩子,在插件初始化时调用
buildStart() {
console.log("🚀 ~ buildStart");
},
// 通用钩子,在构建结束时调用
buildEnd() {
console.log("🚀 ~ buildEnd");
},
// 通用钩子
closeBundle() {
console.log("🚀 ~ closeBundle:");
},
};
}
(2)在 vite.config.ts 中引入,关键代码:
typescript
import { defineConfig, ConfigEnv, UserConfig, loadEnv } from "vite";
import testPlugin from "./plugins/vite-plugin-test.ts"
export default defineConfig(({ command, mode }: ConfigEnv): UserConfig => {
return {
plugins: [vue(), testPlugin()],
}
});

如果修改了代码,热重启的话,就会多出 buildEnd、closeBundle、buildStart 这几个钩子函数的触发。
在开发模式下,closeBundle 实际上 仅在服务关闭时执行一次;若在重启服务时看到两次,是正常现象。

10.3 编写一个打包计时插件
(1)创建src/plugins/vite-plugin-build-time.ts:
typescript
import { Plugin, ResolvedConfig } from "vite";
export default function viteBuildTimePlugin(): Plugin {
let config: ResolvedConfig | undefined;
let startTime: number;
let endTime: number;
return {
name: "vite-build-time-plugin",
configResolved(resolvedConfig: ResolvedConfig) {
config = resolvedConfig;
},
//打包开始,可以使用buildStart钩子
buildStart() {
console.log(`🚀 ~ 欢迎使用系统! 正在为您
${config!.command === "build" ? "打包" : "开发编译"}`);
if (config!.command === "build") {
startTime = Date.now();
}
},
closeBundle() {
if (config!.command === "build") {
endTime = Date.now();
console.log(`👏🏻 ~ 打包完成,耗时${endTime - startTime}毫秒`);
}
},
};
}
(2)vite.config.ts 中引入,同时注释上一个测试插件,关键代码:
typescript
import { defineConfig, ConfigEnv, UserConfig, loadEnv } from "vite";
import viteBuildTimePlugin from "./plugins/vite-plugin-build-time";
export default defineConfig(({ command, mode }: ConfigEnv): UserConfig => {
return {
plugins: [
vue(),
viteBuildTimePlugin(),
],
}
});


10.4 修改html的内容(transformIndexHtml)
transformIndexHtml :转换 index.html 的专用钩子。钩子接收当前的 HTML 字符串和转换上下文。上下文在开发期间暴露ViteDevServer实例,在构建期间暴露 Rollup 输出的包。
(1)创建 vite-plugin-html-title.ts:
typescript
import { Plugin } from "vite";
export default function viteHTMLTitlePlugin({ title = "" }): Plugin {
return {
name: "vite-html-title-plugin",
// 插件的应用顺序
// https://vitejs.cn/vite6-cn/guide/api-plugin.html#plugin-ordering
enforce: "pre",
// apply 属性有 build 和 serve 两种模式,分别代表打包和开发模式,默认为两者都适用
apply: "serve",
transformIndexHtml(html) {
return html.replace(/<title>(.*?)<\/title>/, `<title>${title}</title>`);
},
};
}
(2)引入,关键代码:
typescript
import { defineConfig, ConfigEnv, UserConfig, loadEnv } from "vite";
import viteBuildTimePlugin from "./plugins/vite-plugin-build-time";
import viteHTMLTitlePlugin from "./plugins/vite-plugin-html-title";
export default defineConfig(({ command, mode }: ConfigEnv): UserConfig => {
return {
plugins: [
vue(),
viteBuildTimePlugin(),
viteHTMLTitlePlugin({ title: "My HTML" }),
],
}
});

10.5 传入模块时调用的钩子
- resolveId:定义一个自定义解析器。解析器可以用于定位第三方依赖项等。这里的 source 就是导入语句中的导入目标,例如:
typescript
import { foo } from '../bar.js';
这个 source就是 ../bar.js.
- load:定义一个自定义加载器。options.attributes 包含导入此模块时使用的导入属性,由第一个解析此模块的 resolveId 钩子或第一个导入中存在的属性确定。
- transform:可以被用来转换单个模块。
10.5.1 将插件作为虚拟模块暴露(比如默认暴露一个函数)
参考:https://vitejs.cn/vite6-cn/guide/api-plugin.html#virtual-modules-convention
虚拟模块是一种很实用的模式,使你可以对使用 ESM 语法的源文件传入一些编译时信息。
(1)创建 src/plugins/vite-plugin-virtual-module.ts:
typescript
import { Plugin } from "vite";
// 虚拟模块的名称
const virtualFibModuleId = "virtual:fib";
// 虚拟模块的名称需要做一下处理
// Vite中约定,对于虚拟模块,解析后的路径需要加上'\0'前缀
const resolvedVirtualFibModuleId = `\0${virtualFibModuleId}`;
export default function vitePluginVirtualModule(): Plugin {
return {
name: "vite-plugin-virtual-module",
resolveId(id) {
if (id === virtualFibModuleId) {
return resolvedVirtualFibModuleId;
}
},
load(id) {
if (id === resolvedVirtualFibModuleId) {
return `export default function fib(n){
if (n < 2) {
return n;
}
return fib(n - 1) + fib(n - 2);
}`;
}
},
};
}
(2)vite.config.ts 中引入,关键代码:
typescript
import { defineConfig, ConfigEnv, UserConfig, loadEnv } from "vite";
import viteBuildTimePlugin from "./plugins/vite-plugin-build-time";
import viteHTMLTitlePlugin from "./plugins/vite-plugin-html-title";
import viteVirtualModulePlugin from "./plugins/vite-plugin-virtual-module";
export default defineConfig(({ command, mode }: ConfigEnv): UserConfig => {
return {
plugins: [
vue(),
viteBuildTimePlugin(),
viteHTMLTitlePlugin({ title: "My HTML" }),
viteVirtualModulePlugin(),
],
}
});
(3)App.vue 中引入插件虚拟模块,关键代码:
typescript
<script lang="ts" setup>
import vfib from "virtual:fib";
console.log(vfib(10));
</script>
(4)vite-env.d.ts 新增模块声明:
typescript
declare module "virtual:*" {
export default any;
}
(5)运行 pnpm dev

10.5.2 结合策略模式,暴露多个虚拟模块函数
(1)下载后续用到的包,解决反序列化时的循环嵌套问题
typescript
pnpm add json-stringify-safe -D
pnpm add @types/json-stringify-safe -D
(2)修改 vite-pugin-virtual-module.ts:
typescript
// 策略模式
const virtualModules: VirtualModule = {
"\0virtual:fib": () =>
`export default function fib(n){ return n < 2 ? n : fib(n - 1) + fib(n - 2); }`,
"\0virtual:config": (config?: ResolvedConfig) =>
`export default function config() { return ${stringify(config)}; }`,
};
export default function vitePluginVirtualModule(): Plugin {
let config: ResolvedConfig | undefined;
return {
name: "vite-plugin-virtual-module",
configResolved(resolvedConfig) {
config = resolvedConfig;
},
resolveId(id) {
if (id.startsWith(prefix)) {
return `\0${id}`;
}
},
load(id) {
if (id.startsWith(`\0${prefix}`)) {
return virtualModules[id](config);
}
},
};
}
(3)App.vue 中使用,关键代码:
typescript
<script lang="ts" setup>
import vfib from "virtual:fib";
import config from "virtual:config";
console.log(vfib(10));
console.log(config());
</script>
(4)运行 pnpm dev

配置信息就可以在浏览器中输出了。
vite 系列文章: