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

How do I create a Vue 3 custom element, including child component styles?

我尝试了 Vue 的 defineCustomElement() 创建自定义元素,但由于某些原因子组件样式未包含在影子根中。

然后我尝试使用 native Element.attachShadow() API instead of using defineCustomElement() (based on a 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 中创建一个包含所有样式的自定义元素?

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() 中执行以下操作:

    1. 创建一个临时应用程序实例,为 mountedunmounted 生命周期挂钩添加 mixin
    2. mounted 挂钩中,将组件自己的样式从 this.$.type.styles 插入到 DOM 中的 <style> 标记中。对 this.$options.components 映射中的组件定义执行相同操作。
    3. unmounted 挂钩中,删除从 mounted 插入的 <style> 标签。
    4. 将临时应用程序实例的 _contextgetCurrentInstance() 复制到当前应用程序上下文中。
    5. Return 组件的渲染函数。
    // 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"> 替换为自定义元素(例如,名为“my-custom-element”):

    之前:

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

    之后:

    // public/index.html
    <body>
      <my-custom-element></my-custom-element>
    </body>
    
  • 而不是 createApp(),使用上面的 defineCustomElement() 创建您的应用程序的自定义元素:

    之前:

    // 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('my-custom-element', defineCustomElement(App))
    

demo