【小程序 - 大智慧】Expareser 组件渲染框架

news2024/9/30 7:19:46

5.png


目录

  • 问题思考
  • 课程目标
  • Web Component
    • 类型说明
    • 定义组件
    • 属性添加
  • Shadow DOM
  • Template and Slot
  • Exparser 框架原理
    • 自定义组件
    • 内置组件
  • 下周计划


问题思考

首先,给大家抛出去几个问题:

  1. 前端框架 Vue React 都有自己的组件库,但是并不兼容,那么 不依赖框架能 自定义组件 吗?
  2. 微信小程序开发的时候都会自定义组件是吧,那么调试控制台出现的 shadow-root 是什么,有注意吗?
  3. 微信小程序编写 wxml 的时候,为什么和 html 语法不一致,多出来 view text 这些标签,里面究竟是如何实现的,彼此有什么关联?

课程目标

通过本节课程的学习,希望大家掌握如下的目标:

  1. 弄懂上述问题背后的执行逻辑
  2. 能够利用原生 Web Component 自定义一个简易的组件

Web Component

使用自定义元素 - Web API | MDN

Web Component直译过来就是 web 组件的意思,就是说明离开了前端框架的帮助,我们依然可以用原生组件来进行开发复用。

类型说明

如同官网所说,继承特定元素类得到的组件是 自定义内置元素组件(可以得到特定类型的属性和方法),继承元素基类得到的组件是 独立自定义元素,本质上两种没什么区别,接下来我们重点就放在第二个上面。

定义组件

<button is="my-button-one">内置按钮</button>
<my-button-two></my-button-two>


// 01 定义一个内置元素的按钮
class MyButtonOne extends HTMLButtonElement {
  constructor() {
    self = super();
  }

  // 元素添加到文档调用
  connectedCallback() {
    // 1.创建一个 div
    const div = document.createElement("div");

    // 2.设置 div 的样式
    div.style.width = "100px";
    div.style.height = "50px";
    div.style.textAlign = "center";
    div.style.lineHeight = "50px";
    div.style.cursor = "pointer";
    self.style.marginBottom = "20px";

    // 3.设置 div 的内容
    div.innerHTML = "自定义按钮";

    // 4.将 div 添加到页面
    self.appendChild(div);
  }
}

// 02 定义一个自定义的按钮
class MyButtonTwo extends HTMLElement {
  constructor() {
    // 先调用父类构造器,实例化 HTMLElement ,这样才能有 html 元素的基本属性
    super();
  }

  // 元素添加到文档调用
  connectedCallback() {
    console.log("自定义元素添加到页面", this);

    // 1.创建一个 div
    const div = document.createElement("div");

    // 2.设置 div 的样式
    div.style.width = "100px";
    div.style.height = "50px";
    div.style.backgroundColor = "red";
    div.style.color = "white";
    div.style.textAlign = "center";
    div.style.lineHeight = "50px";
    div.style.cursor = "pointer";

    // 3.设置 div 的内容
    div.innerHTML = "自定义按钮";

    // 4.将 div 添加到页面
    this.appendChild(div);
  }

  // 元素从文档中移除时调用
  disconnectedCallback() {
    console.log("自定义元素从页面移除");
  }

  // 元素被移动到新文档时调用
  adoptedCallback() {
    console.log("自定义元素被移动到新文档");
  }

  // 监听属性变化
  attributeChangedCallback(name, oldValue, newValue) {
    console.log(`属性 ${name} 已由 ${oldValue} 变更为 ${newValue}`);
  }
}

// 组件注册
customElements.define("my-button-one", MyButtonOne, { extends: "button" });
customElements.define("my-button-two", MyButtonTwo);

// 监听组件状态
customElements.whenDefined("my-button-two").then(() => {
  console.log("my-button-two 组件已定义");
});

自定义组件的命名规则是有限制的:

  • 自定义元素的名称,必须包含短横线(-)。它可以确保html解析器能够区分常规元素和自定义元素,还能确保html标记的兼容性。
  • 自定义元素只能一次定义一个,一旦定义无法撤回。
  • 自定义元素不能单标记封闭。比如 <custom-component />,必须写一对开闭标记。比如 <custom-component></custom-component>

上面两个就是最基本的自定义组件,但是这个也没有样式 class 属性传值 事件方法都没有,下面我们一步步加上。

属性添加

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <style>
      * {
        margin: 0;
        padding: 0;
      }

      body {
        width: 100px;
        margin: 200px auto;
        background-color: #f5f5f5;
      }

      .my-button-two {
        width: 180px;
        height: 50px;
        background-color: red;
        color: white;
        text-align: center;
        line-height: 50px;
        cursor: pointer;
      }
    </style>
    <my-button-two color="pink" text="Custom Component" @click="clickButton()"></my-button-two>
    <title>02_属性添加</title>
  </head>
  <body>
    <script>
      // 自定义方法
      const clickButton = () => {
        alert("点击了自定义按钮");
      };

      class MyButtonTwo extends HTMLElement {
        // 监控属性变化
        static observedAttributes = ["color", "text", "@click"];

        constructor() {
          // 先调用父类构造器,实例化 HTMLElement ,这样才能有 html 元素的基本属性
          super();
        }

        // 元素添加到文档调用
        connectedCallback() {
          // 1.创建一个 div
          const div = document.createElement("div");

          // 2.设置 div 的样式
          div.className = "my-button-two";

          // 3.设置 div 的内容
          const bgColor = this.getAttribute("color");
          const textValue = this.getAttribute("text");
          const clickValue = this.getAttribute("@click");

          // 需要在同一个 js 执行环境内部执行
          div.addEventListener("click", () => {
            eval(clickValue);
          });

          div.style.backgroundColor = bgColor;
          div.innerHTML = textValue;

          // 4.将 div 添加到页面
          this.appendChild(div);
        }

        // 元素从文档中移除时调用
        disconnectedCallback() {
          console.log("自定义元素从页面移除");
        }

        // 元素被移动到新文档时调用
        adoptedCallback() {
          console.log("自定义元素被移动到新文档");
        }

        // 监听属性变化
        attributeChangedCallback(name, oldValue, newValue) {
          console.log(`属性 ${name} 已由 ${oldValue} 变更为 ${newValue}`);
        }
      }

      customElements.define("my-button-two", MyButtonTwo);

      // 监听组件状态
      customElements.whenDefined("my-button-two").then(() => {
        console.log("my-button-two 组件已定义");
      });
    </script>
  </body>
</html>

对着调试控制台我们可以发现,当前 html 写的样式可以影响到组件内部,这并不符合我们之前说的组件和外部彼此 属性隔离 的特点,这就需要了解到下一个概念了。

Shadow DOM

使用影子 DOM - Web API | MDN

影子 DOM(Shadow DOM)允许你将一个 DOM 树附加到一个元素上,并且使该树的内部对于在页面中运行的 JavaScriptCSS 是隐藏的。

在这里插入图片描述

有一些 影子 DOM 术语 需要注意:

  • 影子宿主(Shadow host):影子 DOM 附加到的常规 DOM 节点。
  • 影子树(Shadow tree):影子 DOM 内部的 DOM 树。
  • 影子边界(Shadow boundary):影子 DOM 终止,常规 DOM 开始的地方。
  • 影子根(Shadow root):影子树的根节点。

这里的 影子宿主(Shadow host) 可以选取普通的 div 标签,但是由于我们是自定义元素,这里的 挂载节点 就是 自定义组件 Web Component 了,接下来我们举一个例子:

const shadow = this.attachShadow({ mode: "open" });


// 这里的 this 就是标识 自定义组件 DOM 元素
// mode 分为 open closed 表示能否通过 dom.shadowRoot 获取
// 不能获取的话,只能在内部通过 shadow 访问了
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <style>
      * {
        margin: 0;
        padding: 0;
      }

      body {
        width: 100px;
        margin: 200px auto;
        background-color: #f5f5f5;
      }

      .my-button-two {
        width: 180px;
        height: 50px;
        background-color: red;
        color: white;
        text-align: center;
        line-height: 50px;
        cursor: pointer;
      }
    </style>
    <my-button-two color="pink" text="Custom Component" @click="clickButton()"></my-button-two>
    <title>03_shadow dom</title>
  </head>
  <body>
    <script>
      // 自定义方法
      const clickButton = () => {
        alert("点击了自定义按钮");
      };

      class MyButtonTwo extends HTMLElement {
        // 监控属性变化
        static observedAttributes = ["color", "text", "@click"];

        constructor() {
          // 先调用父类构造器,实例化 HTMLElement ,这样才能有 html 元素的基本属性
          super();
        }

        // 元素添加到文档调用
        connectedCallback() {
          // 隔离 DOM
          const shadow = this.attachShadow({ mode: "open" });

          // 1.创建一个 div
          const div = document.createElement("div");

          // 2.设置 div 的样式
          div.className = "my-button-two";

          // 3.设置 div 的内容
          const bgColor = this.getAttribute("color");
          const textValue = this.getAttribute("text");
          const clickValue = this.getAttribute("@click");

          // 需要在同一个 js 执行环境内部执行
          div.addEventListener("click", () => {
            eval(clickValue);
          });

          div.style.backgroundColor = bgColor;
          div.innerHTML = textValue;

          // 4.将 div 添加到页面
          shadow.appendChild(div);
        }

        // 元素从文档中移除时调用
        disconnectedCallback() {
          console.log("自定义元素从页面移除");
        }

        // 元素被移动到新文档时调用
        adoptedCallback() {
          console.log("自定义元素被移动到新文档");
        }

        // 监听属性变化
        attributeChangedCallback(name, oldValue, newValue) {
          console.log(`属性 ${name} 已由 ${oldValue} 变更为 ${newValue}`);
        }
      }

      customElements.define("my-button-two", MyButtonTwo);

      // 监听组件状态
      customElements.whenDefined("my-button-two").then(() => {
        console.log("my-button-two 组件已定义");
      });
    </script>
  </body>
</html>

这里我们可以看到 文档的样式已经无法影响我们的自定义组件了,这是因为被 shadow 阻隔了,接下来就可以继续完善这段逻辑了。

Template and Slot

使用模板和插槽 - Web API | MDN

前端组件开发中有两套我们熟悉的 Template(模板)和 Slot(插槽),接下来就利用这两个功能继续完善一下我们的代码逻辑。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <style>
      * {
        margin: 0;
        padding: 0;
      }

      body {
        width: 100px;
        margin: 200px auto;
        background-color: #f5f5f5;
      }

      .my-button-two {
        width: 180px;
        height: 50px;
        background-color: red;
        color: white;
        text-align: center;
        line-height: 50px;
        cursor: pointer;
      }
    </style>
    <template id="button-template">
      <style>
        .my-button-two {
          width: 180px;
          height: 50px;
          background-color: red;
          color: white;
          text-align: center;
          line-height: 50px;
          cursor: pointer;
        }
      </style>
      <div class="my-button-two">
        <slot name="text"></slot>
      </div>
    </template>
    <my-button-two id="my-button-two" color="pink" @click="clickButton()"
      ><span slot="text">Custom Component</span></my-button-two
    >
    <title>04_Tempalte and Slot</title>
  </head>
  <body>
    <script>
      // 自定义方法
      const clickButton = () => {
        alert("点击了自定义按钮");
      };

      class MyButtonTwo extends HTMLElement {
        // 监控属性变化
        static observedAttributes = ["color", "text", "@click"];

        constructor() {
          // 先调用父类构造器,实例化 HTMLElement ,这样才能有 html 元素的基本属性
          super();
        }

        // 元素添加到文档调用
        connectedCallback() {
          // 隔离 DOM
          const shadow = this.attachShadow({ mode: "closed" });

          // 1.获取模板
          const template = document.querySelector("#button-template");

          // 2.克隆模板
          const content = template.content.cloneNode(true);

          // 3.显示文本
          const clickValue = this.getAttribute("@click");

          // 4.执行函数
          const clickEvent = content.querySelector(".my-button-two");
          clickEvent.addEventListener("click", () => {
            eval(clickValue);
          });

          // 5.将 template 添加到页面
          shadow.appendChild(content);
        }

        // 元素从文档中移除时调用
        disconnectedCallback() {
          console.log("自定义元素从页面移除");
        }

        // 元素被移动到新文档时调用
        adoptedCallback() {
          console.log("自定义元素被移动到新文档");
        }

        // 监听属性变化
        attributeChangedCallback(name, oldValue, newValue) {
          console.log(`属性 ${name} 已由 ${oldValue} 变更为 ${newValue}`);
        }
      }

      customElements.define("my-button-two", MyButtonTwo);

      // 监听组件状态
      customElements.whenDefined("my-button-two").then(() => {
        console.log("my-button-two 组件已定义");
      });
    </script>
  </body>
</html>

艺龙酒店科技官网

举例 video 标签就是利用这套机制封装的…

Exparser 框架原理

Exparser 是微信小程序的组件组织框架,内置在小程序基础库中,为小程序提供各种各样的组件支撑。内置组件和自定义组件都有 Exparser 组织管理。

Exparser 的组件模型与 WebComponents 标准中的 Shadow DOM 高度相似,Exparser 会维护整个页面的节点树相关信息,包括节点的属性、事件绑定等,相当于一个简化版的 Shadow DOM 实现。Exparser 的主要特点包括以下几点:

  • 基于 Shadow DOM 模型:模型上与 WebComponentsShadow DOM 高度相似,但不依赖浏览器的原生支持,也没有其他依赖库;实现时,还针对性地增加了其他 API 以支持小程序组件编程。
  • 可在纯 JS 环境中运行:这意味着逻辑层也具有一定的组件树组织能力。
  • 高效轻量:性能表现好,在组件实例极多的环境下表现尤其优异,同时代码尺寸也较小。

自定义组件

上图是小程序利用 shadow dom 实现 样式和JS 逻辑隔离的组件,这只是第一层,里面的 view text 也是由 Exparser 从普通 div span 封装得来的,接下来让我们深入了解下:

内置组件

接下来带大家一步步过一遍微信小程序内置组件是如何渲染的

// 1.在微信开发工具找到解析命令 wcc
// wcc 是将 wxml 解析为 js 文件,然后逻辑线程注入 webview 执行的
微信web开发者工具\code\package.nw\node_modules\wcc-exec

// 2.将命令文件移动到文件目录下,开始执行解析
./wcc -js index.wxml >> dom.js

  • 可以看到本质上就是一个封装好的 $gwx 函数,它的作用是生成微信自定义的组件和虚拟 dom 节点( diff 算法),用来给后面的 Exparser 生成真实的 DOM 节点
  • 那这个函数是在哪里调用的呢,我们继续向下看
// 1. 调试控制台打开当前页面的 webview
document.getElementsByTagName('webview')
document.getElementsByTagName('webview')[0].showDevTools(true, null)

// 2. 可以发现编译后的 wxml 会利用 js 脚本以一定格式插入到页面中执行
var decodeName = decodeURI("./pages/command_component/index.wxml")
var generateFunc = $gwx(decodeName)

generateFunc()

// 3.传入数据
generateFunc({logs:[1,2,3]})


<view wx:for="{{ logs }}" wx:key="index">
  <text>{{ item }}</text>
</view>

可以看到如上图所示的虚拟节点数组,接下来我们详细剖析一下

  1. $gwx(decodeName) 不直接返回 dom 树,而是返回一个函数的原因是因为需要动态注入和相关配置,函数能够很好的把控时机
  2. 利用动态传参我们发现,包含循环数组和 key 的会带有 virtual 标识,用来后面的 DIff 算法比较
  3. document.dispatchEvent 触发自定义事件 将 generateFunc 当作参数传递给底层渲染库

  1. 可以看得到无论是 view 还是 text 底层都是通过 div span 的自定义组件构成的
  2. 这一切来源于 Exparser 框架,在 渲染层 会内置一系列方法,大致和上面自定义 web component 一致,进行对组件的定义,注册后将 js 脚本引入页面,那么当前页面就可用了
  3. 接下来带大家进行源码的拆解

下周计划

  1. 继续深入小程序原理(收益不高)
  2. 扩展前端其他的技术方向(感兴趣建议)
    1. 前端组件库实现拆解
    2. 前端调试能力提升
    3. 前端工程化能够了解

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2166866.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Vxe UI vue vxe-table vxe-grid 单元格与表尾单元格如何格式化数据

Vxe UI vue vxe-table vxe-grid 单元格与表尾单元格如何格式化数据 查看 github vxe-table 官网 单元格内容格式化 通过 formatter 属性自定义格式化方法 <template><div><vxe-grid v-bind"gridOptions"></vxe-grid></div> </t…

软考高级:SOA 和微服务 AI 解读

概念讲解 SOA&#xff08;面向服务架构&#xff09;和微服务虽然都是服务架构的设计模式&#xff0c;但它们的侧重点和实现方式有很大区别。为了帮助你理解这两个概念&#xff0c;我们可以从生活中的例子、概念本身的讲解以及记忆方法三方面入手。 生活化例子 **SOA&#xf…

python高级用法_闭包

闭包 在函数嵌套的前提下&#xff0c;内部函数使用了外部函数的变量&#xff0c;并且外部函数返回了内部函数&#xff0c;我们把这个使用外部函数变量的内部函数称为闭包 简单闭包 def outer(logo):def inner(msg):print("<%s>%s<%s>" % (logo, msg, …

JSON与CSV之间的主要区别

今天要和大家深入探讨一个数据处理中的常见问题——JSON与CSV之间的主要区别。这两种数据格式各有千秋&#xff0c;适用于不同的场景。让我们一起来了解它们的特点和应用。 一、数据结构的差异 首先&#xff0c;JSON是一种轻量级的数据交换格式&#xff0c;能够表示复杂的数据…

【JAVA基础】JAVA类的拷贝使用示例

文章目录 一、框架介绍二、性能对比三、易用性对比四、使用示例&#xff08;一&#xff09;Apache Commons BeanUtils 使用例子1、第一个例子&#xff1a;两个对象属性个数和名称一样&#xff0c;复制过程2、第二个例子&#xff1a;属性个数和名称不一样&#xff0c;复制过程 &…

简易STL实现 | 红黑树的实现

1、原理 红黑树&#xff08;Red-Black Tree&#xff09;是一种自平衡的二叉搜索树 红黑树具有以下特性&#xff0c;这些特性保持了树的平衡&#xff1a; 节点颜色&#xff1a; 每个节点要么是红色&#xff0c;要么是黑色根节点颜色&#xff1a; 根节点是黑色的。叶子节点&…

信息收集---CDN指纹识别

1. 什么是CDN 在渗透测试过程中&#xff0c;经常会碰到网站有CDN的情况。CDN即内容分发网络&#xff0c;主要解决因传输距离和不同运营商节点造成的网络速度性能低下的问题。说的简单点&#xff0c;就是一组在不同运营商之间的对接点上的高速缓存服务器&#xff0c;把用户经常访…

蓝牙技术|蓝牙6.0技术或将实现厘米级精确查找定位功能

蓝牙技术联盟发布蓝牙 6.0 技术规范&#xff0c;引入了一项名为蓝牙“信道探测”的新功能&#xff0c;可以在两个蓝牙 LE 设备之间实现双向测距&#xff0c;有望为电子设备上的“查找”功能带来更精确的定位能力。蓝牙技术联盟表示&#xff0c;这项新技术将为数十亿未来的蓝牙设…

深度学习:调整学习率

目录 前言 一、什么是调整学习率&#xff1f; 二、调整学习率的作用 三、怎么调整学习率 1.有序调整 2.自适应调整 3.自定义调整 4.调整示例 前言 在深度学习中&#xff0c;调整学习率是非常重要的&#xff0c;它对模型的训练效果和收敛速度有显著影响。 一、什么是调整…

前端问答:如何用 JavaScript 让 HTML Canvas全屏显示

哈喽&#xff0c;大家好&#xff01;今天要跟大家分享一个非常实用的小技巧&#xff0c;适合那些正在学习前端开发的朋友们。你是不是也遇到过这样的问题&#xff1a;在制作一些网页小游戏、炫酷的网页动画或者数据可视化时&#xff0c;想让画布&#xff08;Canvas&#xff09;…

Ubuntu24.04 yum安装

安装yum&#xff1a; sudo apt-get install yum 执行报错 E: Package yum has no installation candidate 解决&#xff1a;更换镜像源&#xff0c;找到自己的系统版本&#xff08;如本系统为Ubuntu24.04&#xff09;用vim进行更换&#xff0c;网址&#xff1a; ubuntu | 镜…

26 基于STM32的智能门禁系统(指纹、蓝牙、刷卡、OLED、电机)

目录 一、主要功能 二、硬件资源 三、程序编程 四、实现现象 一、主要功能 基于STM32单片机&#xff0c;六个按键&#xff0c;分别代表指纹、蓝牙、刷卡的正确进门与错误进门&#xff1b; 比如第一个按键按下&#xff0c;表示指纹正确&#xff0c;OLED显示指纹正确&#x…

linux服务器部署filebeat

# 下载filebeat curl -L -O https://artifacts.elastic.co/downloads/beats/filebeat/filebeat-7.17.23-linux-x86_64.tar.gz # 解压 tar xzvf filebeat-7.17.23-linux-x86_64.tar.gz# 所在位置&#xff08;自定义&#xff09; /opt/filebeat-7.17.23-linux-x86_64/filebeat.ym…

FreeRTOS——任务调度、任务状态

任务调度 调度器就是使用相关的调度算法来决定当前需要执行哪个任务。 FreeRTOS一共支持三种任务调度方式&#xff1a; 抢占式调度&#xff1a;主要是针对优先级不同的任务&#xff0c;每个任务都有一个优先级&#xff0c;优先级高的任务可以抢占优先级低的任务。&#xff08…

word批量裁剪图片,并调整图片大小,不锁定纵横比

在word中有若干图片待处理&#xff0c;裁剪出指定内容&#xff0c;调整成指定大小。如下是待处理的图片&#xff1a; 这时&#xff0c;选择视图&#xff0c;选择宏&#xff0c;查看宏 选择创建宏 添加cut_picture代码如下&#xff0c;其中上、下、左、右裁剪的橡塑尺寸根据自己…

【2】图像视频的加载和显示

文章目录 【2】图像视频的加载和显示一、代码在哪写二、创建和显示窗口&#xff08;一&#xff09;导入OpenCV的包cv2&#xff08;二&#xff09;创建窗口&#xff08;三&#xff09;更改窗口大小 & 显示窗口&#xff08;四&#xff09;等待用户输入补充&#xff1a;ord()函…

24最新ComfyUI搭建使用教程

前言 ComfyUI 是一个基于节点流程式的stable diffusion AI 绘图工具WebUI&#xff0c; 通过将stable diffusion的流程拆分成节点&#xff0c;实现了更加精准的工作流定制和完善的可复现性。 ComfyUI因为内部生成流程做了优化&#xff0c;生成图片时的速度相较于WebUI有10%~25…

关于字节 c++

字节的介绍 字节是计算机中最小的存储单位&#xff0c;通常由8个二进制位组成&#xff0c;用来存储一个字符。在C中&#xff0c;字节也是基本数据类型之一&#xff0c;用关键字"byte"来表示。字节主要用于存储一些较小的数据&#xff0c;如整数、字符等。字节的大小…

音频转MP3格式困难?如何轻松实现wav转mp3?

格式多样化为我们带来了灵活性和创意的无限可能&#xff0c;但同时&#xff0c;不同格式间的转换也成为了不少用户面临的难题。尤其是当你手握珍贵的WAV音频文件&#xff0c;却希望它们能在更多设备上流畅播放或节省存储空间时&#xff0c;wav转mp3的需求便应运而生。WAV以其无…

网络安全中的 EDR 是什么:概述和功能

专业知识&#xff1a;EDR、XDR、NDR 和 MDR_xdr edr ndr-CSDN博客 端点检测和响应 (EDR) 是一种先进的安全系统&#xff0c;用于检测、调查和解决端点上的网络攻击。它可以检查事件、检查行为并将系统恢复到攻击前的状态。EDR 使用人工智能、机器学习和威胁情报来避免再次发生攻…