一、技术点引入
上一篇文章讲了如何通过vite插件获取到被引入的组件样式,并插入到引入的自定义元素中,这一篇文章将探讨一下如何获取到antdv的样式。
这里以第二篇文章中的CodeEditor.ce.vue自定义元素为例,其中使用了a-textarea这个组件,但是他的样式丢失掉了;然而当我们去翻antdv的组件源码时,我们会看到这样:
他没有提供打包好的静态css资源,只提供了一堆方法,而要判断怎么调用,调用哪些方法是很浪费心智的一件事,并且我们也不可能针对每一个组件单独写一个导入方法。
如何才能够拿到打包好的css文件,我在antdv官网看到了ssr中style的样式资源引入:
可以看到,利用antdv针对ssr的支持,我们可以将一些组件样式给烘焙出来,然后将烘焙好的style字符串添加到自定义组件中。
二、 代码实现
下面是antdvCssLoader插件的代码:
js
import type { PluginOption } from 'vite'
import { h, type RendererElement, type RendererNode, type VNode } from 'vue'
// 将antdv所有组件全部导入进来,然后按需使用
import * as antdv from 'ant-design-vue'
import { renderToString } from '@vue/server-renderer'
const { cssinjs } = antdv
const { createCache, extractStyle, StyleProvider } = cssinjs
export default function vitePluginVueAntdvCssLoader(prefix = 'a'): PluginOption {
return {
name: 'vite-plugin-vue-antdv-css-loader',
enforce: 'pre',
transform(code, id) {
if (/\.ce.vue$/.test(id)) {
// 获取全部的组件名
const componentNames = getAntdvCompName(code, prefix)
// 这个是要烘培的组件数组
const renderComponents: VNode<RendererNode, RendererElement, { [key: string]: any }>[] = []
// 从antdv中根据组件名拿到对应组件
componentNames
.forEach(componentName => {
const Component = antdv[componentName]
// 如果有setup方法才代表他是一个组件,防止拿到的不是组件
if (Component.setup) {
renderComponents.push(h(Component))
}
})
if (renderComponents.length) {
// 下面是烘培样式,可以将烘培后的样式存放到cache中
const cache = createCache()
renderToString(h(StyleProvider, { cache }, () => renderComponents))
const styleText = extractStyle(cache)
// 直接将style标签拼接到代码后面
code = `${code}${styleText}`
return code
}
}
},
}
}
// 获取所有的组件名
function getAntdvCompName(str: string, prefix: string): string[] {
const components: Set<string> = new Set();
// 匹配所有的<script>标签
const scriptRegex = /<script[^>]*>((.|\n|\r)*?)<\/script>/g
const scriptMatches = str.match(scriptRegex)
scriptMatches && scriptMatches.forEach((scriptStr) => {
// 移除所有的多行注释
const noMultilineComments = scriptStr.replace(/\/\*[^*]*\*+([^/*][^*]*\*+)*\//g, '')
// 移除所有的单行注释
const noComments = noMultilineComments.replace(/\/\/.*/g, '')
// 匹配import引入的组件
const importRegex = /import\s*{([^}]*)}\s*from\s*['"]ant-design-vue['"]/g
const importMatches = noComments.match(importRegex)
const compNameRegex = /import\s*{([^}]*)}\s*from\s*['"]ant-design-vue['"]/
// 获取名称
importMatches && importMatches.forEach(comp => {
const compNameMatch = comp.match(compNameRegex)
compNameMatch && compNameMatch[1].split(',')?.forEach(name => (name && components.add(name.trim())))
})
});
// 匹配所有的 < template > 标签
const templateRegex = /<template[^>]*>((.|\n|\r)*?)<\/template>/g
const templateMatches = str.match(templateRegex)
templateMatches && templateMatches.forEach((templateStr) => {
// 删除所有注释
const noComments = templateStr.replace(/<!--[\s\S]*?-->/g, '')
// 匹配直接使用组件库组件的行
const compTabRegex = new RegExp(`<${prefix}-([a-z]+)[^>]*>`, 'g')
const nameMatchs = noComments.match(compTabRegex)
const compNameRegex = new RegExp(`<${prefix}-([\\w-]+)`, 's')
nameMatchs && nameMatchs.forEach(compTab => {
const compNameMatch = compTab.match(compNameRegex)
if (compNameMatch) {
let element = compNameMatch[1];
// 将首字母大写
element = element.charAt(0).toUpperCase() + element.slice(1);
components.add(element)
}
})
})
return Array.from(components)
}
其中主要也是利用正则匹配,从script标签的引入组件和直接使用的组件都进行匹配,这是为了适配antdv的新特性:自动按需引入组件;所以这个插件也是允许用户传递一个prefix ,他将要保持与antdv中的prefix一致:
这样才能够让正则表达式正确的匹配到引入的组件。 这时可以看看效果,这是引入之前的CodeEditor自定义元素:
可以看到,正确的设置了类名,但是挂载不到样式,style标签也只有组件自带的样式和统一处理删除符号的style样式。
这是使用插件之后的:
可以看到样式被很好的应用上了,样式标签也被很好的插入进去。
三、总结
- 这个解决办法是带有一定探索性质的。首先,它的解决办法只能针对当前版本,无法做到向前向后适配;其次,它只适用于antdv,其他的组件库必须得提供像antdv一样的ssr样式烘培的解决办法,并且针对不同的组件库还要对插件进行不同的修改,我能提供的只是一种解决问题的思想。
- antdv的样式非常的大,我们可以在样式复制前后输出观察一下:
可以看到差不多膨胀了20倍;这代表我们的项目在打包后会存在很多重复的无意义的样式散落在各个自定义元素中,就像这样:
这是在没有开启样式自动导入的打包:
这是开启样式自动导入的打包:
可以看到,仅仅是将一个Textarea组件 的css静态打包,就多了将近30KB,如果在其他自定义组件也有用到Textarea组件,则又会增加30KB;而antdv也没有提供一个运行时获取样式的办法,虽然他在每次不同类型组件被挂载到dom树上时就会将对应的样式挂载到header中,就像这样:
但是他也没有提供一个标识来告知哪一个标签是属于哪一个组件的,更有可能一个组件会用到好几个标签中的样式,这就很难在运行时判断出该从哪一个style标签中复制样式到自定义元素内。所以在自定义元素中使用第三方组件库要谨慎,很有可能会影响到静态资源打包后的大小。
- 根据以上信息,我推测很多组件库不愿给webcomponent做适配也是源于这个两难的点:如果在编译时进行css资源的载入,则会让包变得很大,而且变大的原因是里面充满了很多重复的style内容;如果在运行时进行css资源载入,则需要每个组件都向外抛出一个方法,能够获取到组建的样式,这样可能会带来很多人力上的消耗,所以不愿意去做这件事。