前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >原生JS实现组件式开发

原生JS实现组件式开发

作者头像
玖柒的小窝
发布2021-12-07 12:41:31
3.6K0
发布2021-12-07 12:41:31
举报
文章被收录于专栏:各类技术文章~

自定义标签

自定义标签通过扩展一个HTMLElementHTMLElement的子类来定义一个新的html标签,是通过原生js实现的组件化。

自定义标签通过window.customElements.define来定义,

  • 第一个参数是标签的名字,必须带有一个短横线-且全是小写字母
  • 第二个参数是标签的构造函数,就是上面提到的继承自HTMLElement的类
  • 第三个参数接收一个对象,当前只有一个extends属性可以配置,如果构造函数是继承自HTMLElement的子类,如HTMLDivElement就需要指定extends:"div"

在定义好自定义元素后就可以直接在html中使用自定义的元素了,如果自定义元素继承自其它元素,需要使用原来的标签加上is属性指定自定义标签的名字

代码语言:javascript
复制
<!-- 继承自HTMLElement -->
<ce-myelement></ce-myelement>

<!-- 继承自p标签 -->
<p is="ce-my-p-element"></p>
复制代码

下面是一个简单例子,点击元素后这个元素会打印出自己

代码语言:javascript
复制
class CopyCode extends HTMLElement {
    constructor() {
        super();
        this.onclick = (e) => {
            if (e.target != this) return;
            console.log(e.target);
        };
    }
}

window.customElements.define("ce-myelement", CopyCode);
复制代码

影子DOM

创建

前面的自定义标签只是定义了自己的一些特别的通用方法,也能插入子元素,已经拥有了组件化的方法,但和复杂的组件相比是完全不够用的,它应该配合另一个特性Shadow DOM一起使用

Shadow DOM能封闭内部,让js和css都无法选择到内部元素(只是无法选择,还是会显示到页面上),里面可以定义<stype>标签且只会影响到内部样式

通过下面方法就能将一个普通元素接管为影子DOM

代码语言:javascript
复制
const innerNode = document.createElement("p");
innerNode.innerText = "inner";

const div = document.querySelector("div");

const shadow = div.attachShadow({ mode: "closed" });

shadow.appendChild(div.appendChild(innerNode));
复制代码

将一个元素设置为shadow DOM后,它的所有子元素都会被页面隐藏,shadow DOM中的元素会出现在屏幕上

通过原来的元素的shadowRoot属性能获得其中的影子DOM,如果创建时mode属性为closed则不能获得影子DOM,这意味着这个元素是完全封闭的,外部无法更改它

代码语言:javascript
复制
const shadow = div.attachShadow({ mode: "closed" });
console.log(div.shadowRoot); // null

const shadow = div.attachShadow({ mode: "open" });
div.shadowRoot == shadow; // true
复制代码

添加样式

通过上面方法已经给div创建了shadowDOM,现在就能向其中添加元素和样式了,样式和普通的页面一样创建

  • 通过创建<style>标签使用innerText手动写
  • 通过cssimport url()方法引入外部样式
  • 通过<link>标签引入外部样式

插槽

通过影子dom接管了普通元素的内部内容,元素中原来的内容都会被隐藏起来,这时可以通过插槽元素<slot>来将外部元素引入影子dom,让它在适当的地方显示出来

一个简单的例子,让div中的文字换成红色的h1大小的文字

代码语言:javascript
复制
const div = document.querySelector("div");
const shadow = div.attachShadow({ mode: "open" });

const h1 = document.createElement("h1");
h1.style.color = "red";
h1.appendChild(slot);

shadow.appendChild(h1);
复制代码

插槽也支持命名插槽,通过在<slot>上定义name属性指定名字,在普通元素上使用slot属性指定同名的插槽,就会把普通元素替换到影子中,同时<slot>中也可以放入默认的元素

代码语言:javascript
复制
const div = document.querySelector("div");
const shadow = div.attachShadow({ mode: "open" });

const slot = document.createElement("slot");
slot.setAttribute("name", "h1");

const h1 = document.createElement("h1");
h1.style.color = "red";
h1.appendChild(slot);
shadow.appendChild(h1); // 在影子中加入一个含插槽的元素

const text = document.createElement("div");
text.innerText = "h1";
text.setAttribute("slot", "h1");
div.appendChild(text); // 将指定了插槽的元素放入原来的元素中
复制代码

模板

上面例子中一直使用代码构建dom树,其实可以使用<templates>标签来构造模板,和普通标签不同,<templates>标签中的内容不会显示到页面上,同时也和影子DOM一样有css的作用域

将上面的代码改写成模板的形式:

代码语言:javascript
复制
<div>aaa</div>

<template id="text">
    <b slot="h1">text</b>
</template>

<template id="temp">
    <style>
        h1 {
            color: red;
        }
    </style>
    <h1>
        <slot name="h1"></slot>
    </h1>
</template>

<script src="./index.js" type="module"></script>
复制代码
代码语言:javascript
复制
const div = document.querySelector("div");
const shadow = div.attachShadow({ mode: "open" });
shadow.appendChild(document.querySelector("#temp").content.cloneNode(true));

const text = document.querySelector("#text").content;
div.appendChild(text);
复制代码

组件

这样,结合上面的自定义标签,就可以制作一个组件了

代码语言:javascript
复制
<body>
    <ce-red-h1 data-text="abc">
        <b slot="h1">b</b>
    </ce-red-h1>

    <template id="temp">
        <style>
            h1 {
                color: red;
            }
        </style>
        <h1>
            <slot name="h1"></slot>
        </h1>
    </template>

    <script src="./index.js" type="module"></script>
</body>
复制代码
代码语言:javascript
复制
class RedH1 extends HTMLElement {
    text;
    constructor() {
        super();
        const template = document.querySelector("#temp");
        this.attachShadow({ mode: "open" }).appendChild(template.content.cloneNode(true));
        this.text = this.dataset.text;
        const p = document.createElement("p");
        p.innerText = this.text;
        this.shadowRoot.appendChild(p);
    }
}
window.customElements.define("ce-red-h1", RedH1);
复制代码

虽然自定义标签也能通过data-来传递数据,但只能是字符串

Vue中使用

文档

vue中提供了一个defineCustomElement来创建一个自定义标签的构造函数,它接收defineComponent相同的参数,返回的类需要使用window.customElements.define来注册,因为是使用原生的方法注册,这样的组件不需要挂载为全局组件就能全局使用,通过vue模板来创建的自定义标签能支持传递对象等复杂数据

在vue中使用自定义标签得先配置loader,否则会有警告提示标签不是vue组件

代码语言:javascript
复制
// vite
vue({
    template: {
        compilerOptions: {
            isCustomElement: (tag) => tag.startsWith("ce-"), // 自定义标签开头用`ce-`这样就不会抛错
        },
    },
})
// vuecli
module.exports = {
  chainWebpack: config => {
    config.module
      .rule('vue')
      .use('vue-loader')
      .tap(options => ({
        ...options,
        compilerOptions: {
          // 将所有以 ion- 开头的标签作为自定义元素处理
          isCustomElement: tag => tag.startsWith('ion-')
        }
      }))
  }
}
复制代码

为了防止打包时将样式单独打包到外部,需要将vue文件后缀名改为.ce.vue

通过单文件组件定义的内容全都放入了自定义元素的影子DOM中

代码语言:javascript
复制
<template>
    <h1 class="redH1">
        <slot name="h1"></slot>
    </h1>
    <p>{{ text.value }}</p>
</template>

<script setup lang="ts">
interface Props {
    text: {
        value?: string;
    };
}
withDefaults(defineProps<Props>(), {
    text: () => ({
        value: "a",
    }),
});
</script>

<style>
// 因为不会被打包到外部且样子只会应用于影子中,所以不用加scoped
h1 {
    color: red;
}
</style>
复制代码
代码语言:javascript
复制
// index.ts
import { defineCustomElement } from "vue";
import RedH1 from "./RedH1.ce.vue";

window.customElements.define("ce-red-h1", defineCustomElement(RedH1));
复制代码

然后在main中通过副作用引入index就能在全局使用了

代码语言:javascript
复制
<template>
    <ce-red-h1 .text="text">
        <b slot="h1">b</b>
    </ce-red-h1>
</template>

<script setup lang="ts">
import { reactive } from "@vue/reactivity";

const text = reactive({
    value: "abc",
});
</script>
复制代码

注意,如果是传递对象,数组等数据,不是使用v-bind:text,而是v-bind:text.prop,简写. 使用单文件时会打包更多的代码进去,如果只是使用简单的功能组件更推荐使用原生写法

使用场景

如果需要扩展从外部获取的html并添加比较复杂的功能,自定义标签就是个很好的选择,比如我的博客的文章通过markdown解析为html,只需要在解析出的html文本的代码片段的右上角的复制按钮就是一个自定义标签,通过自定义点击事件直接将父元素中的innerText复制进剪贴板,就不用像思否的粘贴按钮一样单独设置每个代码片段的粘贴内容

本文系转载,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文系转载前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 自定义标签
  • 影子DOM
    • 创建
      • 添加样式
        • 插槽
        • 模板
        • 组件
        • Vue中使用
        • 使用场景
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档