Web Components详解-组件通信

简介: Web Components详解-组件通信

前言

我们常说到程序的运行和代码的实现遵循高内聚和低耦合,理解一下这句话,模块中的功能在逻辑上是有关联的,模块之间依赖关系较弱。前端的组件同样遵循这套原则,单个组件的功能逻辑是完整的,组件与组件之间也没有强关联,那么如何保证组件之间的联系呢?在Vue和React中一般使用props响应式通信、bus事件总线、Pinia,Vuex,Mobx全局状态等等方式进行数据传递,类似的本篇文章也将介绍Web组件的通信方式

插槽(Slots)

插槽的使用在之前的文章介绍过,通过自定义标签中其他标签的slot属性与影子DOM的slot标签绑定达到传递组件的效果,本文就不做介绍

属性(Attributes)

在介绍创建自定义标签时,我们曾经接触过属性监听器函数attributeChangedCallback,通过在静态方法observedAttributes中声明属性的列表,来监听某个或者某些属性的变化,在标签使用setAttribute函数时会触发属性监听回调。借助这个特点,我们结合代理(proxy)或者存取器(set,get)将element.attrs = value的操作也代理到标签中,思考下面的代码

// 组件工厂
const createCustomElement = (config) => {
  const {
    name,
    attrs = [],
    mode = "open",
    temp,
    parent = document.body,
    BaseClass = HTMLElement,
  } = config;
  let elem = document.createElement(name);
  customElements.define(
    name,
    // @ts-ignore
    class extends BaseClass {
      constructor() {
        super();
        this.initProxy();
        this.attachShadow({ mode });
        this.shadowRoot?.appendChild(temp.content);
      }
      initProxy() {
        // 监听设置标签属性操作
        elem = new Proxy(elem, {
          set: (target, property, value) => {
            target.setAttribute(property, value);
            return Reflect.set(target, property, value);
          },
          get: (target, property) => {
            const obj = Reflect.get(target, property);
            // 事件函数优化,取this指向问题
            if (typeof obj === "function") return obj.bind(this);
            return obj;
          },
        });
      }
      attributeChangedCallback(attrName, oldValue, newValue) {
        console.log(`${attrName}属性的旧值:${oldValue},新值:${newValue}`);
      }
      static observedAttributes = attrs;
    }
  );
  parent.appendChild(elem);
  return elem;
};

我们实现一个组件的批量创建的函数,无需每次都重新使用自定义标签创建组件,只需要调用createCustomElement函数就可以创建一个组件。创建组件操作如下

<!DOCTYPE html>
<html lang="en">
 
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Attributes</title>
</head>
 
<body>
    <template id="temp1">
        <div>
            123
        </div>
    </template>
    <template id="temp2">
        <div>
            456
        </div>
    </template>
    <script src="./helpers.js"></script>
    <script>
        const attrs = ["attrs"]
        const elemName1 = "custom-element1"
        const elemName2 = "custom-element2"
        const elem1 = createCustomElement({
            name: elemName1,
            attrs,
            temp: temp1
        })
        const elem2 = createCustomElement({
            name: elemName2,
            attrs,
            temp: temp2
        })
        elem1.attrs = 111
 
    </script>
</body>
 
</html>

当我们使用elem1.attrs = 111给elem1的属性赋值时,会调用自定义标签中的attributeChangedCallback函数,达到组件的信息传递的效果

事件(Events)

讲完了基础的属性传递,我们来试试通过标签自定义事件(CustomEvent)的方式给组件添加信息传递的途径。

自定义事件有三个知识点,分别是CustomEvent类,dispatchEvent函数以及addEventListener函数,后两者不难理解,它们是element上的方法,用于触发和监听标签的事件。而CustomEvent类的作用则是承载数据的传递及事件的配置。

CustomEvent构造函数接收两个参数,第一个是string类型的type,表示事件类型;第二个是eventInitDict,表示事件配置,其中包含bubbles(允许冒泡),cancelable(允许取消,通过event.preventDefault()可以取消该方法调用),composed(允许穿越Shadow DOM的边界),detail(自定义事件传递的额外数据)。

其中composed在实现自定义组件时尤为重要,将其设置为true时才能在外界和影子DOM中传递数据,参考下面的一段关于自定义事件的代码

<!DOCTYPE html>
<html lang="en">
 
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Events</title>
</head>
 
<body>
    <template id="temp1">
        <button id="btn1">
            组件1
        </button>
        <script type="module">
            const root = elem1.shadowRoot
            root.querySelector("#btn1").addEventListener('click', () => {
                // 在按钮点击时触发自定义事件
                root.dispatchEvent(new CustomEvent('customClick', {
                    bubbles: true, // 允许事件冒泡
                    composed: true, // 允许事件穿越Shadow DOM边界
                    detail: { message: '点击' } // 传递的数据
                }));
            });
        </script>
    </template>
    <template id="temp2">
        <button>
            组件2
        </button>
    </template>
    <script src="./helpers.js"></script>
    <script>
        const elemName1 = "custom-element1"
        const elemName2 = "custom-element2"
        const elem1 = createCustomElement({
            name: elemName1,
            temp: temp1
        })
        const elem2 = createCustomElement({
            name: elemName2,
            temp: temp2,
            BaseClass: class extends HTMLElement {
                connectedCallback() {
                    this.addEventListener('customClick:elem1', (e) => {
                        // 监听、接收来自elem1的customClick:elem1事件消息
                        console.log("收到elem1的消息:", e.detail);
                    });
                }
            }
        })
        elem1.addEventListener('customClick', (e) => {
            // 监听、接收来自elem1的customClick事件消息,并发送customClick:elem1消息给elem2
            elem2.shadowRoot.dispatchEvent(new CustomEvent('customClick:elem1', e));
        });
    </script>
</body>
 
</html>

当点击组件1时,控制台会打印相关信息,达到组件通信的效果

消息中心(MessageCenter)

消息中心类似vue的$emit,它是一种发布订阅的写法,在异步,解耦的实践中发挥重要的作用,在早期的文章中,我曾经介绍过这种设计模式的实现:消息中心发布者/订阅者模式。本文就不做详细的介绍,有兴趣的朋友可以看看链接文章,下面给出消息中心通信方式的使用示例:

<!DOCTYPE html>
<html lang="en">
 
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Events</title>
</head>
 
<body>
    <template id="temp1">
        <button id="btn1">
            组件1
        </button>
        <script type="module">
            const root1 = elem1.shadowRoot
            bus.on(`${elemName2}:msg`, (e) => {
                console.log(e);
            })
            root1.querySelector("#btn1").addEventListener('click', () => {
                bus.emit(`${elemName1}:msg`, { msg: "hello this is elem1" })
            });
        </script>
    </template>
    <template id="temp2">
        <button id="btn2">
            组件2
        </button>
        <script type="module">
            const root2 = elem2.shadowRoot
            bus.on(`${elemName1}:msg`, (e) => {
                console.log(e);
            })
            root2.querySelector("#btn2").addEventListener('click', () => {
                bus.emit(`${elemName2}:msg`, { msg: "hello this is elem2" })
            });
        </script>
    </template>
    <script src="./helpers.js"></script>
    <script src="./node_modules/event-message-center/dist/umd/index.js"></script>
    <script>
        const elemName1 = "custom-element1"
        const elemName2 = "custom-element2"
        const bus = MessageCenter.messageCenter
        const elem1 = createCustomElement({
            name: elemName1,
            temp: temp1
        })
        const elem2 = createCustomElement({
            name: elemName2,
            temp: temp2
        })
 
    </script>
</body>
 
</html>

上述代码中,当我们点击组件1时,控制台打印hello this is elem1;点击组件2时提示hello this is elem2,由此达到组件的消息通信

全局状态(GloalState)

我要介绍的全局状态可能与框架中常用的全局状态管理工具不太一样,这里我使用代理实现了一个简单的状态管理类,以供组件可以访问

// 简单的响应式全局状态
class GlobalState {
  constructor(state, action) {
    this.state = this.initState(state);
    this.action = action;
  }
  initState(state) {
    return new Proxy(state, {
      set: (target, key, val) => {
        Reflect.set(target, key, val);
        this.action(target);
        return true;
      },
    });
  }
}

在组件中,通过修改state的状态触发钩子函数,达到响应的效果

<!DOCTYPE html>
<html lang="en">
 
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>GloalState</title>
</head>
 
<body>
    <template id="temp1">
        <button id="btn1">
            组件1
        </button>
        <script type="module">
            const root1 = elem1.shadowRoot
            root1.querySelector("#btn1").addEventListener('click', () => {
                state.name = "elem1"// 组件1中点击,name转换成elem1
            });
        </script>
    </template>
    <template id="temp2">
        <button id="btn2">
            组件2
        </button>
        <script type="module">
            const root2 = elem2.shadowRoot
            root2.querySelector("#btn2").addEventListener('click', () => {
                state.name = "elem2"// 组件2中点击,name转换成elem2
            });
        </script>
    </template>
    <script src="./helpers.js"></script>
    <script src="./node_modules/event-message-center/dist/umd/index.js"></script>
    <script>
        const elemName1 = "custom-element1"
        const elemName2 = "custom-element2"
        const { state } = new GlobalState({
            name: "elem"
        }, (state) => {
            console.log(state);
        })
        console.log(state);// 初始化name是elem
        const elem1 = createCustomElement({
            name: elemName1,
            temp: temp1
        })
        const elem2 = createCustomElement({
            name: elemName2,
            temp: temp2
        })
 
    </script>
</body>
 
</html>

总结

在前端开发中,遵循高内聚低耦合的原则,确保组件之间的功能逻辑关联性较强,同时组件之间的耦合性较低,是设计和构建可维护性高的应用的重要指导原则。

Web 组件作为可重用的组件化技术,也需要有效的通信机制来实现组件之间的数据传递和交互。文章中的每种通信方式都有其适用的场景,根据项目需求和设计原则来选择合适的通信方式是非常重要的。

以上就是文章全部内容了,如果觉得文章不错的话,还望三连支持一下,感谢!

相关文章
|
22天前
|
JavaScript 前端开发 开发者
哇塞!Vue.js 与 Web Components 携手,掀起前端组件复用风暴,震撼你的开发世界!
【8月更文挑战第30天】这段内容介绍了Vue.js和Web Components在前端开发中的优势及二者结合的可能性。Vue.js提供高效简洁的组件化开发,单个组件包含模板、脚本和样式,方便构建复杂用户界面。Web Components作为新兴技术标准,利用自定义元素、Shadow DOM等技术创建封装性强的自定义HTML元素,实现跨框架复用。结合二者,不仅增强了Web Components的逻辑和交互功能,还实现了Vue.js组件在不同框架中的复用,提高了开发效率和可维护性。未来前端开发中,这种结合将大有可为。
65 0
|
23天前
|
Web App开发 JavaScript 前端开发
[译] 用 Web Worker 改善 Vue 组件性能
[译] 用 Web Worker 改善 Vue 组件性能
|
2月前
|
Web App开发 前端开发 安全
2024年新一代WebOffice内嵌网页组件,Web网页在线编辑Word/Excel/PPT
WebOffice控件面临兼容性、用户体验和维护难题。随着浏览器更新,依赖插件的技术不再适用,如Chrome不再支持NPAPI和PPAPI。产品普遍不支持多版本Office并存,定制能力弱,升级复杂。猿大师办公助手提供了解决方案,它兼容多种浏览器,包括最新版和国产浏览器,不依赖插件,支持文档对比,具有丰富的功能和接口,兼容多种Office版本,允许源码级定制,提供终身技术支持,并实现静默在线升级。适用于多种行业和操作系统。
114 2
|
1月前
|
数据可视化 数据挖掘 持续交付
Axure Web端元件库:从Quick UI到500+组件的飞跃
在快速变化的数字世界中,产品设计不仅仅是功能的堆砌,更是用户体验的精心雕琢。原型设计作为产品开发过程中的关键环节,其重要性不言而喻。Axure,作为业界领先的原型设计工具,凭借其强大的交互设计和丰富的功能,赢得了全球设计师和开发者的信赖。而Axure Web端元件库,则是这一平台上的一颗璀璨明珠,它以超过500个精心设计的组件为基础,为设计师们打开了一扇通往高效、高质量原型设计的大门。
|
1月前
|
开发框架 .NET API
.Net Core Console 项目如何使用 HttpClient 与 Web 服务通信
.Net Core Console 项目如何使用 HttpClient 与 Web 服务通信
|
2月前
|
前端开发 Python
前后端分离的进化:Python Web项目中的WebSocket实时通信解决方案
【7月更文挑战第18天】在Python的Flask框架中,结合Flask-SocketIO库可轻松实现WebSocket实时通信,促进前后端分离项目中的高效交互。示例展示了一个简单的聊天应用:Flask路由渲染HTML,客户端通过Socket.IO库连接服务器,发送消息并监听广播。此方法支持多种实时通信协议,适应不同环境,提供流畅的实时体验。
66 3
|
2月前
|
JavaScript 前端开发 UED
WebSocket在Python Web开发中的革新应用:解锁实时通信的新可能
【7月更文挑战第16天】WebSocket是实现Web实时通信的协议,与HTTP不同,它提供持久双向连接,允许服务器主动推送数据。Python有多种库如websockets和Flask-SocketIO支持WebSocket开发。使用Flask-SocketIO的简单示例包括定义路由、监听消息事件,并在HTML中用JavaScript建立连接。WebSocket提高了实时性、减少了服务器压力,广泛应用于聊天、游戏等场景。
33 1
|
2月前
|
移动开发 前端开发 网络协议
Python Web实时通信新纪元:基于WebSocket的前后端分离技术探索
【7月更文挑战第16天】WebSocket增强Web实时性,Python借助Flask-SocketIO简化实现。安装`flask`和`flask-socketio`,示例展示服务器端接收连接及消息并广播响应,前端HTML用Socket.IO库连接并监听事件。WebSocket开启双向通信新时代,助力动态Web应用开发。
43 1
|
2月前
|
JavaScript 前端开发 API
Web Components详解-HTML Templates
Web Components详解-HTML Templates
37 6
|
2月前
|
JavaScript 前端开发
Web Components详解-Shadow DOM样式控制
Web Components详解-Shadow DOM样式控制
62 3