Vite 深度剖析(二)

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://tailwindcss.com/

中文官网: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 系列文章:

Vite 深度剖析(一)

Vite 深度剖析(二)

Vite 深度剖析(三)

Vite 深度剖析(四)

相关推荐
Sheldon一蓑烟雨任平生3 小时前
Vite 深度剖析(一)
vue·react·vite·环境变量·esbuild·vite.config.ts·依赖预构建
walking9572 天前
Vite 打包优化终极指南:从 30MB 到 800KB 的性能飞跃
前端·vue.js·vite
whyfail3 天前
CVE-2026-39363-Vite开发服务器安全漏洞深度分析
安全·vite
江上清风山间明月3 天前
Vite现代化的前端构建工具详解
前端·webpack·nodejs·vite
发现一只大呆瓜6 天前
深入浅出 Tree Shaking:Rollup 是如何“摇”掉死代码的?
前端·性能优化·vite
发现一只大呆瓜6 天前
深度起底 Vite:从打包流程到插件钩子执行时序的全链路解析
前端·vite
发现一只大呆瓜7 天前
深度解密 Rollup 插件开发:核心钩子函数全生命周期图鉴
前端·vite
发现一只大呆瓜7 天前
深度解析 Rollup 配置与 Vite 生产构建流程
前端·vite
小兵阿飞7 天前
Vite 技术介绍:实现原理、应用与优化
前端·vite