如何创建 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()
中执行以下操作:
- 创建一个临时应用程序实例,为
mounted
和 unmounted
生命周期挂钩添加 mixin
。
- 在
mounted
挂钩中,将组件自己的样式从 this.$.type.styles
插入到 DOM 中的 <style>
标记中。对 this.$options.components
映射中的组件定义执行相同操作。
- 在
unmounted
挂钩中,删除从 mounted
插入的 <style>
标签。
- 将临时应用程序实例的
_context
从 getCurrentInstance()
复制到当前应用程序上下文中。
- 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))
我尝试了 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
'scustomElement
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()
中执行以下操作:- 创建一个临时应用程序实例,为
mounted
和unmounted
生命周期挂钩添加mixin
。 - 在
mounted
挂钩中,将组件自己的样式从this.$.type.styles
插入到 DOM 中的<style>
标记中。对this.$options.components
映射中的组件定义执行相同操作。 - 在
unmounted
挂钩中,删除从mounted
插入的<style>
标签。 - 将临时应用程序实例的
_context
从getCurrentInstance()
复制到当前应用程序上下文中。 - 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))