前言
本文描述了将 Vue 组件打包成任何框架可用的 Web Component 方法。与其他文章不同的是,无需使用 Vue 的 defineCustomElement 方法即可实现,这也避免了出现 shadow DOM(shadow Root) 的问题。
此方案具有以下特点:
-
使用Vue编写
-
最终结果编译为WebComponent
-
Webpack打包后浏览器引入即可使用
-
不生成shadowDom,即:外部CSS仍对其有作用、外部可复写CSS样式
背景
众所周不知,最近两年在开发nvPress个人博客系统。该系统可拥有第三方插件、主题功能。在实现插件的时候,为保证其兼容性(在任何前端框架的主题中使用),因此需使用W3C标准的WebComponent来实现。而在开发的过程中,如果使用原生JS编写,则显得非常繁杂。因此考虑使用 Vue 来实现 WebComponent 。并且最后编译成单个的js文件供浏览器直接引入使用。
不使用ShadowDom
通常,要使用Vue编写WebComponent,首先会想到Vue3.2新增的defineCustomElement方法。这也就直接造成了一个弊端:该方法到目前为止都使用shadowDOM并且无法通过参数去除这个特性。这也就造成了以下两个问题:
-
WebComponent无法使用外部样式
-
外部样式无法重写WebComponent样式
虽然这就是shadowDOM出现的初衷,但作为插件来讲,有的时候确实就需要这些功能。因此,直接挂载一个Vue组件会更加方便:
import {nextTick,createApp} from 'vue'
import BlockComponent from './frontend.vue';
customElements.define('nv-block-pandastudio-iframe-modal',class extends HTMLElement {
constructor() {
super();
nextTick(()=>{
var block_data = this.data || {};
createApp(BlockComponent,{
data: block_data,
}).mount(this)
})
}
})
以上代码直接定义了一个WebComponent,然后在使用的时候将Vue组件直接启动为实例并挂载到自定义标签下。
组件数据
在使用Vue.defineCustomElement方法时,组件会自动处理标签的attribute作为prop。而在这里用的是直接挂载,因此需要手动处理一下数据:
-
可能需要nextTick一下,这应该是浏览器在加载到标签首端的时候,就立即应用了自定义方法的结果。而自定义DOM的attributes、props可能都还不存在
-
上述的var block_data = this.data || {}是我自己用来获取组件默认数据的,请根据你数据的实际位置进行调整。如果是渲染在自定义标签上的attributes,你应该使用this.getAttribute()方法来获取。不过这样需要注意得到的数据是String类型。传输给Vue组件使用的时候要做处理
-
Vue的createApp方法第一个参数是组件本身,第二个参数是传输给组件的Props数据(对象)
编译成浏览器引入即可使用的文件
由于这是编译出来直接用于浏览器的插件,基于这个考虑,我对编译的时候有以下要求:
-
JS文件不拆分:毕竟是单个功能模块,就算再大也不会大到哪里去
-
CSS in JS:CSS不单独引入,直接集成到JS里面自动挂载(如果不需要这个功能,可以extract出去)
-
CSS不使用预处理器:根据我个人的使用情况,预处理器通常只是为了帮助我把less或sass的CSS嵌套编译出来。而在2023年12月,所有的浏览器最新版都已经支持了CSS nesting(如果你仍然需要预处理器,可自行配置)
提示
即使使用了JS内嵌CSS,也没有使用预处理器,你仍然可以愉快的使用<style scoped>来做局部CSS
于是我在使用Webpack编写@vue/cli配置时使用了以下参数:
module.exports = defineConfig({
publicPath: "./srcs/",
outputDir: '../srcs/',
transpileDependencies: true,
runtimeCompiler: false,
productionSourceMap: false,
filenameHashing: false,
chainWebpack: (config) => {
config.optimization.delete('splitChunks');
config
.plugin('limitSplitChunks')
.use(webpack.optimize.LimitChunkCountPlugin, [{ maxChunks: 1 }]);
config.plugins.delete('html')
config.plugins.delete('preload')
config.plugins.delete('prefetch')
},
css: {
extract: false
},
configureWebpack: {
entry: "./src/entry.js",
experiments: {
outputModule: true,
},
output: {
filename: 'frontstage.js',
},
},
})
如果要使用Vite做打包配置,也是类似的,这里就不再额外做示例了。
总结
通过createApp后直接mount的方式挂载,可避免shadowRoot。但需在createApp时添加prop参数。最后打包即可。
示例配置下载
下载链接参考
-
CSS Nesting
-
Vue API - createApp()
-
@vue/cli