如何创建 Vue 3 自定义元素,包括子组件样式?

Posted

技术标签:

【中文标题】如何创建 Vue 3 自定义元素,包括子组件样式?【英文标题】:How do I create a Vue 3 custom element, including child component styles? 【发布时间】:2021-12-16 05:25:46 【问题描述】:

我尝试使用Vue的defineCustomElement()创建自定义元素,但由于某种原因,子组件样式没有包含在阴影根中。

然后我尝试使用native Element.attachShadow() API 而不是使用defineCustomElement()(基于Codesandbox)手动创建我的影子根,但是根本没有加载任何样式:

代码main.js

import  createApp  from "vue";
import App from "./App.vue";
import router from "./router";
import store from "./store";

let treeHead = document.querySelector("#app");
let holder = document.createElement("div");
let shadow = treeHead.attachShadow( mode: "open" );
shadow.appendChild(holder);

createApp(App).use(store).use(router).mount(holder);

代码 vue.config.js:

module.exports = 
  chainWebpack: (config) => 
    config.module
      .rule("vue")
      .use("vue-loader")
      .loader("vue-loader")
      .tap((options) => 
        options.shadowMode = true;
        return options;
      );
    config.module
      .rule("css")
      .oneOf("vue-modules")
      .use("vue-style-loader")
      .tap((options) => 
        options.shadowMode = true;
        return options;
      );
    config.module
      .rule("css")
      .oneOf("vue")
      .use("vue-style-loader")
      .tap((options) => 
        options.shadowMode = true;
        return options;
      );
  ,
;

代码 package.json:


  "name": "shadow-root",
  "version": "0.1.0",
  "private": true,
  "scripts": 
    "serve": "vue-cli-service serve",
    "build": "vue-cli-service build"
  ,
  "dependencies": 
    "vue": "^3.2.20",
    "vue-loader": "^16.8.2",
    "vue-router": "^4.0.0-0",
    "vue-style-loader": "^4.1.3",
    "vuex": "^4.0.0-0"
  ,
  "devDependencies": 
    "@vue/cli-plugin-router": "~4.5.0",
    "@vue/cli-plugin-vuex": "~4.5.0",
    "@vue/cli-service": "~4.5.0",
    "@vue/compiler-sfc": "^3.0.0",
    "node-sass": "^4.12.0",
    "sass-loader": "^8.0.2"
  ,
  "browserslist": [
    "> 1%",
    "last 2 versions",
    "not dead"
  ]

如何在 shadow root 中创建一个包含所有样式的自定义元素?

【问题讨论】:

【参考方案1】:

在 Vue 3 中不需要 Vue 配置。只有 Vue 2 中的开发服务器需要它来渲染自定义元素中的样式。

使用defineCustomElement() 是注册自定义元素的推荐方法。但是,在使用 defineCustomElement() 时存在一个未解决的问题,即根本不会呈现嵌套组件样式 (@vuejs/vue-next#4462)。

解决方法是将所有组件作为自定义元素导入,以便将样式附加到组件定义而不是附加到 <head>,然后在安装时将这些样式插入 DOM:

vue.config.js 中启用vue-loader's customElement mode:

// vue.config.js
module.exports = 
  chainWebpack: config => 
    config.module
      .rule('vue')
      .use('vue-loader')
      .tap(options => 
        options.customElement = true
        return options
      )
  

或者,将所有组件文件扩展名从 .vue 重命名为 .ce.vue

创建一个实用函数来包装 Vue 的 defineCustomElement() 并在 setup() 中执行以下操作:

    创建一个临时应用程序实例,为mountedunmounted 生命周期挂钩添加mixin。 在mounted 挂钩中,将组件自己的样式从this.$.type.styles 插入到DOM 中的<style> 标记中。对 this.$options.components 映射中的组件定义执行相同操作。 在unmounted 挂钩中,删除从mounted 插入的<style> 标记。 将临时应用程序实例的_contextgetCurrentInstance() 复制到当前应用程序上下文中。 返回组件的渲染函数。
// defineCustomElementWithStyles.js
import  defineCustomElement as VueDefineCustomElement, h, createApp, getCurrentInstance  from 'vue'

const nearestElement = (el) => 
  while (el?.nodeType !== 1 /* ELEMENT */) el = el.parentElement
  return el


export const defineCustomElement = (component) =>
  VueDefineCustomElement(
    setup() 
      const app = createApp()
      1️⃣
      app.mixin(
        mounted() 
          const insertStyles = (styles) => 
            if (styles?.length) 
              this.__style = document.createElement('style')
              this.__style.innerText = styles.join().replace(/\n/g, '')
              nearestElement(this.$el).prepend(this.__style)
            
          

          2️⃣
          insertStyles(this.$?.type.styles)
          if (this.$options.components) 
            for (const comp of Object.values(this.$options.components)) 
              insertStyles(comp.styles)
            
          
        ,
        unmounted() 
          this.__style?.remove() 3️⃣
        ,
      )

      4️⃣
      const inst = getCurrentInstance()
      Object.assign(inst.appContext, app._context)

      5️⃣
      return () => h(component)
    ,
  )

编辑public/index.html 以将<div id="app"> 替换为自定义元素(例如,名为“app-root”):

之前:

// public/index.html
<body>
  <div id="app"></div>
</body>

之后:

// public/index.html
<body>
  <app-root id="app"></app-root>
</body>

使用上面的defineCustomElement() 代替createApp() 来创建应用的自定义元素:

之前:

// main.js
import  createApp  from 'vue'
import App from './App.vue'
createApp(App).mount('#app')

之后:

// main.js
import  defineCustomElement  from './defineCustomElementWithStyles'
import App from './App.vue'
customElements.define('app-root', defineCustomElement(App))

demo

【讨论】:

以上是关于如何创建 Vue 3 自定义元素,包括子组件样式?的主要内容,如果未能解决你的问题,请参考以下文章

Vue自定义组件父与子

精通系列

精通系列

精通系列

如何正确使用 vue js Web 组件内部的插槽并应用样式

vue里面父组件修改子组件样式的方法