前言
Vite
在2.0版本提供了Library Mode(库模式),让开发者可以使用Vite
来构建自己的库以发布使用。正好我准备封装一个React组件并将其发布为npm包以供日后方便使用,同时之前也体验到了使用Vite
带来的快速体验,于是便使用Vite
进行开发。
背景
在开发完成后进行打包,出现了如图三个文件:
其中的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
,所以需要开启相关选项,当我们的库是由Vue
或React
编写的时候,使用的时候一般也是在该环境下,例如我的这个组件是基于React
进行编写,那么使用时无疑也是在React
中进行引入,这样就会造成产物冗余,所以需要在external
配置中添加上外部化的依赖,以在打包时给剔除掉。output
选项是输出产物为umd格式时(具体格式查看build.lib.formats
选项,umd为Universal Module Definition,可以直接script
标签引入使用,所以需要提供一个全局变量)。
配置完上述提及到的后,我接着寻找与打包样式相关的内容,然而并没有发现。。。
没关系,我们还可以去仓库issues看看,说不定有人也发现了这个问题。搜索后果不其然,底下竟有高达47条评论:
点进去后,提问者问到如何才能不生成CSS文件,尤回答说:进行样式注入的DOM环境会产生服务端渲染的不兼容问题,如果CSS代码不多,使用行内样式进行解决。
这个回答显然不能让很多人满意(这可能是该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
官网,里面的输出钩子章节有这么一张图:
根据上图可以看到输出阶段钩子的执行顺序及其特性,而我们只需要在写入之前拿到输出的产物进行拼接,因此就得用到上面的generateBundle
这个hook。
实现
官方推荐编写的插件是一个返回实际插件对象的工厂函数,这样做的话可以允许用户传入配置选项作为参数来自定义插件行为。
基本结构如下:
import type { Plugin } from 'vite';
function VitePluginStyleInject(): Plugin {
return {
name: 'vite-plugin-style-inject',
apply: 'build', // 应用模式
enforce: 'post', // 作用阶段
generateBundle(_, bundle) {
}
};
}
Vite
默认的formats
有es和umd两种格式,假设不修改该配置将会有两个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
参数,它是一个键由文件名字符串值为AssetInfo
或ChunkInfo
组成的对象,其中一段的内容如下:
上图看出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()],
})
执行构建命令后,只输出两个文件:
引入打包后的文件发现其能正常运行,终于搞定啦~
尾言
完成后回到该issue下厚着脸皮放上项目地址 😁
最后整理了下写了这篇文章,这是我第一次将记录发表成文,感谢您的阅读,觉得有帮助的话就点个👍吧。