首页/文章/vite工具

实现一个打包时将CSS注入到JS的Vite插件

2023-04-20
25059 分钟
...

Vite 在2.0版本提供了Library Mode(库模式),让开发者可以使用Vite来构建自己的库以发布使用。正好我准备封装一个React组件并将其发布为npm包以供日后方便使用,同时之前也体验到了使用Vite带来的快速体验,于是便使用Vite进行开发。

背景

在开发完成后进行打包,出现了如图三个文件:

Image.png

其中的style.css文件里面包含了该组件的所有样式,如果该文件单独出现的话,意味着在使用时需要进行单独引入该样式文件,就像使用组件库时需在主文件引入其样式一样。

import xxxComponent from 'xxx-component';
import 'xxx-component/dist/xxx.css'; // 引入样式 

但我封装的只是单一组件,样式不多且只应用于该组件上,没有那么复杂的样式系统。

所以打包时比较好的做法是配置构建工具将样式注入到JS文件中,从而无需再多一行引入语句。我们知道Webpack打包是可以进行配置来通过一个自执行函数在DOM上创建style标签并将CSS注入其中,最后只输出JS文件,但在Vite的官方文档中似乎并没有告诉我们怎么去配置。

让我们先来看一下官方提供的配置:

// vite.config.js 

import { resolve } from 'path'
import { defineConfig } from 'vite'

export default defineConfig({
  build: {
    lib: {
      entry: resolve(__dirname, 'lib/main.js'),
      name: 'MyLib',
      // the proper extensions will be added 

      fileName: 'my-lib'
    },
    rollupOptions: {
      // make sure to externalize deps that shouldn't be bundled 

      // into your library 

      external: ['vue'],
      output: {
        // Provide global variables to use in the UMD build 

        // for externalized deps 

        globals: {
          vue: 'Vue'
        }
      }
    }
  }
}) 

首先要开启build.lib选项,配置入口文件和文件名等基本配置,由于Vite生产模式下打包采用的是rollup,所以需要开启相关选项,当我们的库是由VueReact编写的时候,使用的时候一般也是在该环境下,例如我的这个组件是基于React进行编写,那么使用时无疑也是在React中进行引入,这样就会造成产物冗余,所以需要在external配置中添加上外部化的依赖,以在打包时给剔除掉。output选项是输出产物为umd格式时(具体格式查看build.lib.formats选项,umd为**Universal Module Definition**,可以直接script标签引入使用,所以需要提供一个全局变量)。

Vite

配置完上述提及到的后,我接着寻找与打包样式相关的内容,然而并没有发现。。。

7625CB0E-F4E2-4AC2-BFFC-DC8C00CD604C.jpeg

没关系,我们还可以去仓库[issues](https://link.zhihu.com/?target=https%3A//github.com/vitejs/vite/issues/1579)看看,说不定有人也发现了这个问题。搜索后果不其然,底下竟有高达47条评论:

Image.png

点进去后,提问者问到如何才能不生成CSS文件,尤回答说:进行样式注入的DOM环境会产生服务端渲染的不兼容问题,如果CSS代码不多,使用行内样式进行解决。

Image.png

这个回答显然不能让很多人满意(这可能是该issue关闭后又重新打开的原因),因为带样式的库在编写过程中几乎不会采用行内的写法,提问者也回复说道那样自己就不能使用模块化的Less了,依旧希望能够给出更多的库模式options,然后下面都各抒己见,但都没有一种很好的解决方案被提出。

因此,为了解决我自己的问题,我决定写一个插件。

Vite Plugin API

Vite插件提供的API实际上是一些hook,其划分为Vite独有hook和通用hook(Rollup的hook,由Vite插件容器进行调用)。这些hook执行的顺序为:

  • Alias
  • 带有 enforce: 'pre' 的用户插件
  • Vite 核心插件
  • 没有 enforce 值的用户插件
  • Vite 构建用的插件
  • 带有 enforce: 'post' 的用户插件
  • Vite 后置构建插件(最小化,manifest,报告)

Vite核心插件基本上是独有hook,主要用于配置解析,构建插件基本上都是Rollup的hook,这才是真正起构建作用的hook,而我们现在想要将获取构建好的CSS和JS产物并将其合二为一,所以编写的插件执行顺序应该在构建的插件执行之后,也就是‘**带有 enforce: 'post' 的用户插件’(输出阶段)**这一阶段执行。

打开Rollup官网,里面的输出钩子部分有这么一张图:

Image.png

根据上图可以看到输出阶段钩子的执行顺序及其特性,而我们只需要在写入之前拿到输出的产物进行拼接,因此就得用到上面的generateBundle这个hook。

实现

官方推荐编写的插件是一个返回实际插件对象的工厂函数,这样做的话可以允许用户传入配置选项作为参数来自定义插件行为。

基本结构如下:

import type { Plugin } from 'vite';

function VitePluginStyleInject(): Plugin {

  return {
    name: 'vite-plugin-style-inject',
    apply: 'build', // 应用模式 

    enforce: 'post', // 作用阶段 

    generateBundle(_, bundle) {

    }
  };
} 

Vite默认的formatsesumd两种格式,假设不修改该配置将会有两个Bundle产生,generateBundle钩子也就会执行两次,其方法的签名及其参数类型为:

type generateBundle = (options: OutputOptions, bundle: { [fileName: string]: AssetInfo | ChunkInfo }, isWrite: boolean) => void;

type AssetInfo = {
  fileName: string;
  name?: string;
  source: string | Uint8Array;
  type: 'asset';
};

type ChunkInfo = {
  code: string;
  dynamicImports: string[];
  exports: string[];
  facadeModuleId: string | null;
  fileName: string;
  implicitlyLoadedBefore: string[];
  imports: string[];
  importedBindings: { [imported: string]: string[] };
  isDynamicEntry: boolean;
  isEntry: boolean;
  isImplicitEntry: boolean;
  map: SourceMap | null;
  modules: {
    [id: string]: {
      renderedExports: string[];
      removedExports: string[];
      renderedLength: number;
      originalLength: number;
      code: string | null;
    };
  };
  name: string;
  referencedFiles: string[];
  type: 'chunk';
}; 

我们只用到其中的bundle参数,它是一个键由文件名字符串值为AssetInfoChunkInfo组成的对象,其中一段的内容如下:

Image.png

上图看出CSS文件的值属于AssetInfo,我们先遍历bundle找到该CSS部分把source值提取出来:

import type { Plugin } from 'vite';

function VitePluginStyleInject(): Plugin {
  let styleCode = '';

  return {
    name: 'vite-plugin-style-inject',
    apply: 'build', // 应用模式 

    enforce: 'post', // 作用阶段 

    generateBundle(_, bundle) {
      // + 遍历bundle 

      for (const key in bundle) {
        if (bundle[key]) {
          const chunk = bundle[key]; // 拿到文件名对应的值 

          // 判断+提取+移除 

          if (chunk.type === 'asset' && chunk.fileName.includes('.css')) {
            styleCode += chunk.source;
            delete bundle[key];
          }
        }
      }
    }
  };
} 

现在styleCode存储的就是构建后的所有CSS代码,因此我们需要一个能够实现创建style标签并将styleCode添加其中的自执行函数,然后把它插入到其中一个符合条件的ChunkInfo.code当中即可:

import type { Plugin } from 'vite';

function VitePluginStyleInject(): Plugin {
  let styleCode = '';

  return {
    name: 'vite-plugin-style-inject',
    apply: 'build', // 应用模式 

    enforce: 'post', // 作用阶段 

    generateBundle(_, bundle) {
      // 遍历bundle 

      for (const key in bundle) {
        if (bundle[key]) {
          const chunk = bundle[key]; // 拿到文件名对应的值 

          // 判断+提取+移除 

          if (chunk.type === 'asset' && chunk.fileName.includes('.css')) {
            styleCode += chunk.source;
            delete bundle[key];
          }
        }
      }

      // + 重新遍历bundle,一次遍历无法同时实现提取注入,例如'style.css'是bundle的最后一个键 

      for (const key in bundle) {
        if (bundle[key]) {
          const chunk = bundle[key];
          // 判断是否是JS文件名的chunk 

          if (chunk.type === 'chunk' &&
            chunk.fileName.match(/.[cm]?js$/) !== null &&
            !chunk.fileName.includes('polyfill')
          ) {
            const initialCode = chunk.code; // 保存原有代码 

            // 重新赋值 

            chunk.code = '(function(){ try {var elementStyle = document.createElement(\'style\'); elementStyle.appendChild(document.createTextNode(';
            chunk.code += JSON.stringify(styleCode.trim());
            chunk.code += ')); ';
            chunk.code += 'document.head.appendChild(elementStyle);} catch(e) {console.error(\'vite-plugin-css-injected-by-js\', e);} })();';
            // 拼接原有代码 

            chunk.code += initialCode;
            break; // 一个bundle插入一次即可 

          }
        }
      }
    }
  };
} 

最后,我们给这个style标签加上id属性以方便用户获取操作:

import type { Plugin } from 'vite';

// - function VitePluginStyleInject(): Plugin { 

function VitePluginStyleInject(styleId: ''): Plugin {
  let styleCode = '';

  return {
    name: 'vite-plugin-style-inject',
    apply: 'build', // 应用模式 

    enforce: 'post', // 作用阶段 

    generateBundle(_, bundle) {
      // 遍历bundle 

      for (const key in bundle) {
        if (bundle[key]) {
          const chunk = bundle[key]; // 拿到文件名对应的值 

          // 判断+提取+移除 

          if (chunk.type === 'asset' && chunk.fileName.includes('.css')) {
            styleCode += chunk.source;
            delete bundle[key];
          }
        }
      }

      // 重新遍历bundle,一次遍历无法同时实现提取注入,例如'style.css'是bundle的最后一个键 

      for (const key in bundle) {
        if (bundle[key]) {
          const chunk = bundle[key];
          // 判断是否是JS文件名的chunk 

          if (chunk.type === 'chunk' &&
            chunk.fileName.match(/.[cm]?js$/) !== null &&
            !chunk.fileName.includes('polyfill')
          ) {
            const initialCode = chunk.code; // 保存原有代码 

            // 重新赋值 

            chunk.code = '(function(){ try {var elementStyle = document.createElement(\'style\'); elementStyle.appendChild(document.createTextNode(';
            chunk.code += JSON.stringify(styleCode.trim());
            chunk.code += ')); ';
            // + 判断是否添加id 

            if (styleId.length > 0)
              chunk.code += ` elementStyle.id = "${styleId}"; `;
            chunk.code += 'document.head.appendChild(elementStyle);} catch(e) {console.error(\'vite-plugin-css-injected-by-js\', e);} })();';
            // 拼接原有代码 

            chunk.code += initialCode;
            break; // 一个bundle插入一次即可 

          }
        }
      }
    }
  };
} 

至此,这个插件就写好了,是不是很简单。

使用

在项目中使用该插件:

// vite.config.js 

import { defineConfig } from 'vite';
import VitePluginStyleInject from 'vite-plugin-style-inject';

export default defineConfig({
  plugins: [VitePluginStyleInject()],
}) 

执行构建命令后,只输出两个文件:

Image.png

引入打包后的文件发现其能正常运行,终于搞定啦~

如果您觉得这篇文章有帮助,请点个赞吧~

分享文章

相关文章

更多文章 →
vite工具2026-03-17
深度解析:NODE_ENV 与 Mode (模式)
在现代前端开发中,尤其是使用 Vite 、 Qwik 、 Next.js 等基于 Node.js 的构建工具时,开发者经常会被两个相似的概念绕晕: 和 Mode (模式) 。 它们看起来都在做同一件事:“区分开发环境和生产环境”。但实际上,它们在架构设计中扮演着截然不同却又紧密协作的角色。混淆这两者可能导致构建配置错误、环境变量加载失败,甚至生产环境泄露敏感信息。 一、核心定义:它们到底是什么? 1\. :行业通用的“运行时开关” 起源...
学习
vite工具2025-10-01
vite常用配置
目录 1\. 基础配置 1.1 项目初始化 1.2 基础 vite.config.js 配置 配置说明与最佳实践 插件配置 (plugins) 好处 : 插件系统是Vite的核心特性,提供了丰富的功能扩展能力 坏处 : 过多插件会增加构建时间,插件冲突可能导致构建失败 建议 : 只引入必要的插件,定期检查插件更新和兼容性 适用场景 : 所有Vite项目都需要根据技术栈选择合适的插件 开发服务器配置 (server) host: ‘0.0...
学习
vite工具2025-06-04
Vite 项目中使用 vite-plugin-dts 插件的详细指南
在现代前端开发中,TypeScript 的类型声明文件( )对于提供良好的开发体验和代码提示至关重要。如果你正在使用 Vite 构建一个库项目, 插件是一个非常有用的工具,它可以帮助你自动生成 文件。以下是如何在你的 Vite 项目中安装、配置和使用 插件的详细指南。 一、插件介绍 是一个用于在 Vite 的库模式下,从 或 源文件生成类型文件( )的插件。它能够帮助开发者在构建库时,自动生成类型声明文件,从而提高开发效率。 二、安装插...
学习
vite工具2025-03-11
vite项目打包build后提示说超过500k了
说可以通过配置build.rollupOptions.output.manualChunks来提升一下,好可以,我来配置一下,把那些可能大的包单独分配一下: 然后再看一下,是变小了,但是还是500警告啊: 那就开始分析是哪个包比较大吧,使用rollup plugin visualizer这个插件来分析一下,rollup plugin visualizer是一个开源项目,地址: 安装: 配置插件 在 文件中添加 插件的配置,如下: 构建完...
学习
vite工具2024-10-10
一次低端机 WebView 白屏的兼容之路
问题 项目:Vite4 + Vue3,APP WebView 项目 页面在 OPPO A5 手机上打不开,页面空白。 最开始是客户端在看,然后发现一个警告,大概也因为没看出什么问题,给到 Web 前端。 相关背景 为了方便描述过程的行为,先做一些相关背景的介绍。知道这些背景才能更好的了解问题的复杂。这些在解决问题的过程中始终是干扰因素,在反复调试试错的过程中才梳理总结出来,这里把它们列出来。 使用测试 App,其中有两个入口,一个是本地...
学习
vite工具2024-09-09
推荐工具vite-plugin-fake-server,可线上部署使用mockmock线上部署
最近遇到了个麻烦,后端迟迟给不了接口,测试那边又要求数据不能写死,必须可以提供mock调试。这不最近找到了一个mock工具,能解决传统mock工具无法线上部署的问题。 工具名字叫做 ,兼容vite,从官网示例上来看是同时支持 和 来模拟数据的。使用起来也非常简单,分三步走就行。 1. 安装 2. 引入 这里贴出最简单的配置,一个是制定文件夹,一个是设置在生产环境下模拟数据。 3. 使用 这里我根据个人习惯,在 文件夹下创建一个 和数个模...
学习

评论

请登录后发表评论

去登录
加载评论中...

目录