前言
Vite在2.0版本提供了库模式,允许开发者使用Vite构建自己的库进行发布和使用。 正好我打算打包一个React组件,发布为npm包,方便以后使用。 之前也体验过使用Vite带来的快速体验,所以就使用Vite进行开发。
背景
开发完成并打包后,有如图所示的三个文件:
style.css 文件包含组件的所有样式。 如果该文件单独出现,则说明使用时需要单独导入样式文件,就像使用组件库时需要在主文件中导入样式一样。
<code class="hljs language-typescript" lang="typescript">import xxxComponent from 'xxx-component';
import 'xxx-component/dist/xxx.css'; // 引入样式
但我封装的只是单个组件,并且没有太多只适用于这个组件的样式。 没有这么复杂的风格系统。
所以,最好配置一个完善的工具,在打包时将样式注入到JS文件中,这样就不需要再引入一行语句了。 我们知道Webpack打包可以配置为通过自执行函数在DOM上创建样式标签并向其中注入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'
}
}
}
}
})
首先需要开启选项,配置入口文件、文件名等基础配置,因为Vite生产模式下使用rollup进行打包,所以需要开启相关选项。 当我们的库是用Vue或者React编译的时候,使用的时候通常也是在这个环境中。 比如我的组件是基于React编译的,那么使用时无疑是在React中引入,这样会导致产品冗余,所以需要在外部配置中添加外部依赖。 包装时可以将其取下。 输出选项是当输出结果为umd格式时(具体格式见build.lib.formats选项,umd是Universal Module Definition,可以直接用script标签导入使用,所以需要一个全局变量提供)。
配置完上面的内容后,我又查找了与包装样式相关的内容,但没有找到。 。 。
没关系,我们也可以去仓库issues看看,说不定有人也发现了这个问题。 搜索的结果出乎意料,下面的评论竟然有47条:
点击进去后,提问者询问如何避免生成CSS文件。 您回复:样式注入的DOM环境会导致服务端渲染不兼容。 如果CSS代码不多,可以使用内联样式来解决。
这个答案似乎让很多人都不满意(这可能是该issue被关闭并重新打开的原因),因为带有样式的库在编译过程中几乎不会使用内联写入,而且提问者也回答说他无法使用了模块化的Less之后,还是希望能够给出更多的库模式选项,然后大家在下面各抒己见,但是还没有提出好的解决方案。
所以,为了解决我自己的问题,我决定写一个插件。
Vite插件API
Vite插件提供的API实际上是一些hook,这些hook被定义为Vite特有hook和通用hook(Rollup hooks,由Vite插件容器调用)。 这些钩子的执行顺序是:
Vite核心插件基本都是特有的钩子,主要用于配置分析,构建插件基本都是Rollup钩子,才是真正在构建中发挥作用的钩子,现在我们要获取构建好的CSS和JS 产品 并将它们合二为一,因此编译后的插件执行顺序应该在创建的插件执行之后执行,即“userplug-inwithenforce:'post'”阶段(输出阶段)。
打开Rollup官网,章节中有这样一张图:
根据上图我们可以看到输出阶段钩子的执行顺序和特点,而我们只需要得到输出产物进行拼接即可进行写入css 包含,所以我们要使用里面的generateBundle钩子。
完成
官方推荐的插件是鞋工厂函数,它返回实际的插件对象,允许用户传入配置选项作为参数来自定义插件的行为。
基本结构如下:
import type { Plugin } from 'vite';
function VitePluginStyleInject(): Plugin {
return {
name: 'vite-plugin-style-inject',
apply: 'build', // 应用模式
enforce: 'post', // 作用阶段
generateBundle(_, bundle) {
}
};
}
Vite 默认格式为 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参数,它是一个对象,其key由文件名字符串值AssetInfo或ChunkInfo组成,其中一节的内容如下:
从上图可以看出,CSS文件的值属于AssetInfo,我们首先遍历bundle找到CSS部分并提取源值:
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 代码,因此我们需要一个自执行函数,可以创建样式标签并向其中添加 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()],
})
执行build命令后,只输出两个文件:
引入打包的文件,发现可以正常运行css 包含,终于搞定了~
结语
完成后,回到issue,厚着脸皮贴上项目地址
最后我整理了一下,写下了这篇文章。 这是我第一次发表唱片。 感谢您的阅读。 如果您觉得有帮助,请点击它。
发表评论