Skip to content

祖传开篇:本文简单介绍vite插件的定义以及如何开发一个简单的vite 插件。作者水平有限,可能比较简陋,也或许有些错误,欢迎指正。

关于vite

官方文档

Vite 是一个现代化的前端构建工具,它的设计理念是为了提供快速的开发体验,尤其在开发过程中,Vite 使用了非常先进的技术栈,如原生 ES 模块支持和高速的 HMR(热模块替换)。Vite 提供了强大的插件机制,使得开发者可以根据需求定制构建过程、扩展功能、优化性能等。

什么是 vite 插件

Vite 插件是对 Vite 构建过程的扩展,它通过实现 Vite 提供的钩子函数,修改 Vite 的行为或添加自定义功能。

基本用于:

  • 修改模块的解析方式。
  • 修改源代码或构建输出。
  • 引入新的构建工具和功能。
  • 改善开发和构建性能。

Vite 插件的 API 非常灵活,并且与 Rollup 插件 API 相似,因为 Vite 本身基于 Rollup 构建工具,所以也能使用 Rollup 的API。

Vite插件开发基础

插件的基本结构

js
export default function myPlugin() {
  return {
    name: 'my-plugin', // 插件的名称
    // 钩子函数...
  };
}
  • name: 每个插件都需要一个独特的名称。
  • 你可以在插件中实现多个钩子函数来定义插件的行为。

插件排序

为了与某些 Rollup 插件兼容,可能需要强制修改插件的执行顺序,或者只在构建时使用。这应该是 Vite 插件的实现细节。可以使用 enforce 修饰符来强制插件的位置:

  • pre:在 Vite 核心插件之前调用该插件
  • 默认:在 Vite 核心插件之后调用该插件
  • post:在 Vite 构建插件之后调用该插件
js
export default function myPlugin() {
  return {
    name: 'my-plugin', // 插件的名称
    enforce: 'pre',
    // 钩子函数...
  };
}
  • Alias (路径别名)相关的插件。
  • ⭐ 带有 enforce: 'pre' 的用户插件。
  • Vite 核心插件。
  • ⭐ 没有 enforce 值的用户插件,也叫 普通插件。
  • Vite 生产环境构建用的插件。
  • ⭐ 带有 enforce: 'post' 的用户插件。
  • Vite 后置构建插件(如压缩插件)

按照不同环境调用

默认情况下插件在开发 (serve) 和生产 (build) 模式中都会调用。如果插件在服务或构建期间按需使用,请使用 apply 属性指明它们仅在 'build' 或 'serve' 模式时调用

js
export default function myPlugin() {
  return {
    name: 'my-plugin', // 插件的名称
    enforce: 'pre',
    apply: 'build',
    // 钩子函数...
  };
}

Vite 与 Rollup 兼容的钩子

  • 服务启动阶段:optionsbuildStart
  • 每个传入模块请求时:当浏览器发起请求获取页面内容时,Vite 依次调用 resolveidloadtransform
  • 服务关闭阶段:依次调用 buildEndcloseBundle

Vite 独有的

  • config 用来进一步修改配置。
  • configResolved 用来记录最终的配置信息。
  • configureServer 用来获取 Vite Dev Server 实例,添加中间件。
  • configurePreviewServer 在其他中间件安装前被调用。 如果想在 其他中间件 之后 安装一个插件 可以返回一个钩子,会在其他中间件安装完成后调用
  • transformIndexHtml 用来转换 HTML 的内容。(这个后面的案例会用到)
  • handleHotUpdate 用来进行热更新模块的过滤,或者进行自定义的热更新处理

官方文档写的很详细,这里就不多写了。 官方文档

执行顺序

  1. 配置阶段
    • config
    • configResolved
  2. 开发服务器阶段(仅开发模式)
    • configureServer
  3. 构建阶段
    • options
    • buildStart
    • resolveId
    • load
    • transform
    • buildEnd
  4. 生成阶段
    • generateBundle
  5. HTML 处理阶段
    • transformIndexHtml

configureServerconfigurePreviewServer 的区别在于前者用于开发模式,后者用于预览模式。

handleHotUpdate 仅在开发模式下生效,因为预览模式不支持热更新。

如果需要更细粒度的控制,可以在 handleHotUpdate 中返回特定的模块列表,或者返回空数组以阻止热更新。

开发两个简单的vite 插件

插件一:内容替换插件

首先来一个简单的内容替换插件 在src下 新建 plugins 目录,新建 my-plugin-console.ts

ts
import type {Plugin} from 'vite';

// 定义插件选项类型
interface myPluginConsoleOptions {
  enabled: boolean;
  style?: string;
  cleanAll?: boolean;
}

export default function myPluginConsole(options: myPluginConsoleOptions): Plugin {
  return {
    name: 'my-plugin-console',
    enforce: 'pre',  // 在 Vite 核心插件之前调用该插件
    apply: 'build',  // 只在构建模式下应用插件
    // 在 vite 构建时,在代码转换阶段调用该插件
    transform(code, id) {
      // 清除所有的console.log
      if (options.cleanAll) {
        code = code.replace(/console\.log\(([^)]*)\)/g, '')
        return {code};
      }
      // 替换console.log的样式(这里使用拼接语法,所以只有基本类型能够log展示)
      if (options.enabled && (id.endsWith('.ts') || id.endsWith('.js') || id.endsWith('.vue'))) {
        code = code.replace(/(console\.log\(([^)]*)\))/g, (_, __, argumentsContent) => {
          return 'console.log("这是修改后的log日志:%c" + ' + argumentsContent + ' , " ' + options.style + ' ");';
        });
        return {code};
      }
      return null;
    }
  };
}

在 vite.config.ts 中引入插件

ts
import {defineConfig} from 'vite'
import vue from '@vitejs/plugin-vue'
import myPluginConsole from './src/plugins/my-plugin-console.ts';

// https://vite.dev/config/
export default defineConfig({
  plugins: [vue(),
    myPluginConsole({
      enabled: true,
      style: 'color:#fff; background: #333;padding:2px;border-radius:3px'
    }),
  ]
})

插件效果:

插件二:图片预加载

图片预加载插件,用于在页面加载时预加载图片,以优化用户体验。 在 src 的 plugins 中新建: vite-plugin-image-prefetch.ts

ts
import type {Plugin} from 'vite';
import fg from 'fast-glob'

interface ImagePrefetchOptions {
  /**
   * 预加载图片的目录
   * @default 'images'
   */
  dir: string[]
  /**
   * 预加载图片的标签
   * prefetch  优先级较低、页面空闲时加载、使用时需要再次请求(走缓存)
   * preload  优先级较高、加载后存到内存中、无须再次请求
   * @default 'prefetch'
   */
  rel: 'prefetch' | 'preload'
  
  /**
   * 构建后图片存放的目录
   * @default 'public'
   */
  buildPublicDir: string
  
  /**
   * 图片最大同时预加载数量
   * @default 6 (浏览器并发最大6个)
   */
  maxLoadSize?: number
  
}

export function imagePrefetch(options: ImagePrefetchOptions): Plugin {
  const {dir, rel, buildPublicDir} = options
  // 给最大加载数量设置默认值,默认为6 也是浏览器并发的上限
  const maxLoadSize = options.maxLoadSize || 6
  let imageArray: string[] = []
  return {
    name: 'vite-plugin-image-prefetch',
    // 获取构建后图片路径
    generateBundle(_, bundle) {
      // 因为可以多路径,这里只需要处理 src路径下的图片,public 中的图片不需要处理
      const dirArray = dir.filter(dirItem => dirItem.includes('src'))
      if (dirArray.length > 0) {
        const values = Object.values(bundle)
        let files: string[] = []
        dirArray.forEach(dirItem => {
          files = [...files, ...fg.sync(dirItem)]
        })
        values.forEach(value => {
          if (files.includes(Reflect.get(value, 'originalFileName'))) {
            imageArray.push(value.fileName)
          }
        })
      }
    },
    transformIndexHtml(html, ctx) {
      // 判断是本地调试还是生产构建,
      // 本地调试则直接获取路径,生产构建需要在generateBundle (rollup 钩子) 中获取构建后路径
      let dirArray = ctx.server ? dir : dir.filter(dirItem => !dirItem.includes('src'))
      if (dirArray.length > 0) {
        dirArray.forEach(dirItem => {
          // 这里获取src路径下的图片时不需要额外拼接 publicDir
          const files = dirItem.includes('src/') ?
              fg.sync(dirItem)
              : fg.sync(dirItem, {
                cwd: ctx.server ? ctx.server.config.publicDir : buildPublicDir
              })
          if (files.length > 0) {
            imageArray = [...imageArray, ...files.map(file => (ctx.server ? ctx.server.config.base : '') + file)]
          } else {
            console.warn(`vite-plugin-image-prefetch dir ${dirItem} is empty`)
          }
        })
        
        // 设置图片预加载并发
        const scriptContent = `
               (function() {
                  const imageArray = ${JSON.stringify(imageArray)};
                  const maxConcurrent = ${maxLoadSize};  // 最大并发数
                  let index = 0;
        
                  const loadImage = (src) => {
                    return new Promise((resolve, reject) => {
                     const link = document.createElement('link')
                     link.rel = '${rel}'
                     link.as = 'image'
                     link.href = src
                     document.head.appendChild(link)
                     link.onload = resolve
                     link.onerror = reject
                    })
                  };

              const loadImagesWithLimit = () => {
                const loadNext = () => {
                  if (index < imageArray.length) {
                    const currentSrc = imageArray[index];
                    index++;
                    loadImage(currentSrc).finally(() => {
                      loadNext();  // 递归加载下一个图片
                    })
                    
                }
              };
          
              // 控制并发数量
              const loadImagePromises = Array.from({ length: maxConcurrent }, loadNext);
            };

            // 开始加载图片
            loadImagesWithLimit();
          })();
        `
        // 将脚本内容添加到 HTML 中
        html = html.replace(
            '</body>', // 在 </body> 标签前插入脚本
            `<script>${scriptContent}</script></body>`
        );
      } else {
        console.warn('vite-plugin-image-prefetch dir is empty')
      }
      return html
    }
  }
  
}

在 vite.config.ts 中引入插件

ts
import {defineConfig} from 'vite'
import vue from '@vitejs/plugin-vue'
import myPluginConsole from './src/plugins/my-plugin-console.ts';
import {imagePrefetch} from './src/plugins/vite-plugin-image-prefetch.ts';

// https://vite.dev/config/
export default defineConfig({
   plugins: [vue(),
      myPluginConsole({
         enabled: true,
         style: 'color:#fff; background: #333;padding:2px;border-radius:3px'
      }),
      imagePrefetch({
         dir: ['image/*.{jpg,png,svg,webp}', 'src/assets/image/*.{jpg,png,svg,webp}'],
         rel: 'preload',
         buildPublicDir: 'public/',
         maxLoadSize: 3
      })
   ]
})

效果展示: