【问题标题】:How do I create a Vue 3 custom element, including child component styles?如何创建 Vue 3 自定义元素,包括子组件样式?
【发布时间】: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 中创建一个包含所有样式的自定义元素?

【问题讨论】:

    标签: javascript vue.js webpack vuejs3 shadow-dom


    【解决方案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() 中执行以下操作:

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

    【讨论】:

      猜你喜欢
      • 2020-01-08
      • 2021-06-06
      • 2021-08-19
      • 2019-10-17
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2019-11-17
      • 1970-01-01
      相关资源
      最近更新 更多