从零开始手写基于Web Components组件

news2024/11/16 10:22:22

前言

本文将详细讲解如何用最基本的typescriptviteless构建基于Web Components的组件,目前已实现ButtonIconImageTabsInputMessage组件。首先介绍项目的整体架构,如何开发第一个Web Components组件——Button,按钮的水波纹特效,实现按钮的disabledtype属性功能。看完一定有所收获~

一.建立项目

主要分为初始化项目和,如何进行开发调试

1.初始化项目

pnpm init 

安装typescript,vite,less,vite-plugin-dts(用于生成ts类型声明文件)

pnpm i typescript vite less vite-plugin-dts -D 

项目目录结构

  • utils:存放一些公共函数
  • index.ts: 打包入口
// index.ts
import './components/button/index.ts'; 
  • components: 组件目录,新建了button目录,button目录下有index.tsindex.less
// components/button/index.tsclass CustomElement extends HTMLElement {constructor() {super();}}customElements.define("r-button", CustomElement); 
  • vite.config.ts: vite的配置文件
import { defineConfig } from "vite";
import { resolve } from "path";
import dts from 'vite-plugin-dts'

export default defineConfig({build: {minify: 'terser',sourcemap: true,lib: {entry: resolve(__dirname, "index.ts"),// 配置入口文件路径name: "ranui",fileName: "index",formats: ["es", "umd"], // 打包生成的格式},},plugins: [dts()]
}); 

2.开发和调试

在项目的根目录中创建一个index.html用于开发和调试,在head标签里面加入

 <script src="./index.ts" type="module"></script> 

配置package.json脚步命令

"scripts": {"dev": "vite","build": "vite build",}, 

使用pnpm dev启动服务的时候,默认访问根目录下的index.html,然而index.html又引入组件库的入口文件index.ts。如果执行无误的话,body标签里面就可以使用我们自定义的标签了。这就可以开发和调试了。

<body><r-button>这是按钮</r-button>
</body> 

自此项目结构搭建完毕,接下来就开始编写button组件代码。 开始写之前先简单回顾下Web Components的一些基础知识。

二.Web Components简述

简单过一下开发Web Components的前置知识,首先是如何创建一个自定义的标签。已经完全掌握的可以跳过,直接看第三章。

1.注册自定义组件

 class CustomElement extends HTMLElement {constructor() {super(); // 必需// 组件被创建时执行的逻辑}// 组件的功能}customElements.define("r-button", CustomElement); 

使用这个自定义组件

<r-button></r-button> 

自定义组件必须使用中横线x-x。创建完了自定义标签,肯定需要加上一些自定义的逻辑和功能,这就会用到组件的生命周期,在对应的时刻,编写相应的逻辑。

2. 组件的生命周期

可以把一些逻辑放到对应的生命周期里,以便在特定的时刻执行。Web Components生命周期有constructor,attributeChangedCallback,connectedCallback,adoptedCallback,disconnectCallback

 class CustomElement extends HTMLElement {constructor() {super(); // 必需// 组件被创建时执行的逻辑}// 组件的功能attributeChangedCallback(){}connectedCallback(){}adoptedCallback(){}disconnectCallback(){}}customElements.define("r-button", CustomElement); 

使用方法:

  • constructor: 元素创建时触发。
  • attributeChangedCallback: 当observedAttributes数组中的属性发生变化时触发。可获得发生变化的属性名,更新后的值,更新前的值。比如
 class CustomElement extends HTMLElement {static get observedAttributes() {return ['type'];}constructor() {super();}attributeChangedCallback(attribute, oldValue, newValue) {if(attribute === 'type') {console.log(newValue, oldValue);}}}customElements.define("r-button", CustomElement); 
  • connectedCallback: 当元素被插入DOM树时触发,此时所有属性和子元素都可用。类似于React中的componentDidMount
  • disconnectCallback:元素从DOM中移除时会调用它。但用户关闭浏览器或者网页的时候,不会触发。
  • adoptedCallback元素通过调用document.adoptNode(element)被插入到文档时触发。目前没用到过。

执行顺序:

constructor->attributeChangedCallback->connectedCallback

3.template模版

如果经常用到同一段HTML代码时,可以用template标签包裹它。通过引用template标签的内容的达到复用的效果。

注意:1.一般都需要用cloneNode方法进行复制,避免引用的都是同一个。2.template标签包裹的内容不会显示在页面中。

下面是两种使用举例。

(1).typescript使用举例:

const template = document.createElement('template');
const slot = document.createElement('slot');
const div = document.createElement('div');
div.setAttribute('class', 'class');
slot.setAttribute('name', 'name');
div.appendChild(slot);
template.appendChild(div);

// 对template进行复制,就可以到处使用了
const content = div.cloneNode(true) as HTMLElement", 

(2).HTML使用举例:

<template id="template"><div>template</div>
</template> 
const template = document.getElementById('template');
const content = template.content.cloneNode(true);
document.body.appendChild(content); 

4. 插槽slots

为了方便向组件内的指定位置传递内容,可以使用slot标签。使用方式也有两种,具名插槽和不具名插槽。

(1).具名插槽

具名插槽会有name属性,根据name属性来匹配插入的位置。

创建一个具有具名插槽的自定义组件r-button

 class Button extends HTMLElement {constructor() {super();const btn = document.createElement('div');btn.setAttribute('class', 'r-btn');const slot = document.createElement('slot')slot.setAttribute('name', 'r-btn_content')btn.appendChild(slot)const shadowRoot = this.attachShadow({ mode: 'closed' });shadowRoot.appendChild(btn); }
 }
 window.customElements.define('r-button', Button); 

使用r-button标签

<r-button><span slot="r-btn_content">这就是具名插槽的作用,根据name属性来匹配插入</span>
</r-button> 

(2).不具名插槽

具名插槽用name属性来匹配总感觉不太灵活,尤其是编写button组件的时候,更希望是

<r-button>这是内容</r-button> 

而不是

<r-button> <span slot="r-btn_content">这是内容</span>
</r-button> 

因此,我们还可以这样使用

 class Button extends HTMLElement {_btn: HTMLDivElement;_iconElement?: HTMLElement;_slot: HTMLSlotElement;constructor() {super();this._slot = document.createElement("slot");this._btn = document.createElement("div");this._btn.setAttribute('class', 'btn')this._btn.appendChild(this._slot);this._slot.setAttribute("class", "slot");const shadowRoot = this.attachShadow({ mode: "closed" });shadowRoot.appendChild(this._btn); }
 }
 window.customElements.define('r-button', Button); 

上述slot标签没有设置name属性,就可以这样使用

<r-button >这是内容</r-button> 

5.Shadow DOM

在给组件增加功能时,往往会编写复杂的DOM结构。这时候就会用到Shadow DOM。它的作用是将一个隐藏的,独立的DOM附加到一个元素上,且具有良好的密封性。

(1).创建一个Shadow DOM到指定的元素上

使用例子:创建一个Shadow DOMCustomElement

 const shadowRoot = CustomElement.attachShadow({ mode: "closed" }); 

attachShadow方法

参数说明类型描述
optionsoptions是一个Object,有一个mode属性,值可以是open或者closed{mode: string}open表示可以通过页面JavaScript来获取shadow DOM ,closed表示不让用JavaScript获取

(2).如何操作shadow DOM

1.获取shadowRoot去操作shadow DOM。操作方式和操作普通DOM的方式一致,都是使用appendChildDOM方法。

const shadowRoot = CustomElement.shadowRoot; 

2.利用css3变量去影响shadow DOM内的样式。

在开发button的水波纹特效时,就会用这个方式。

3.在shadow DOM中添加link或者script去链接其他资源

(3).shadow DOM在页面的样子:

6.customElements兼容性

customElements是创建自定义组件的必备方法,兼容性如下

三.编写最基本的Button组件

掌握了Web Components的基本知识,便开始开发最简单的button组件了。接下来分为,

  • 最基础的默认按钮,
  • 水波纹特效
  • 按钮typedisabled属性

1.默认按钮

<r-button>默认按钮</r-button> 

首先按钮要能传入内容,所以肯定需要slot标签,具体操作如下

// components/button/index.ts

import styles from './index.less'class CustomElement extends HTMLElement {_slot: HTMLSlotElement;constructor() {super();this._slot = document.createElement("slot"); // 创建slot标签this._slot.setAttribute("class", "slot");const shadowRoot = this.attachShadow({ mode: "closed" });const style = document.createElement('style') // 创建样式标签style.textContent = styles // 将导入的less样式加入style标签shadowRoot.appendChild(style) // 将样式加入shadow DOMshadowRoot.appendChild(this._slot); // 将slot加入shadow DOM}} 

编写基本样式,使用css伪类:host选择当前的自定义元素,样式分为下面几块。

 :host {position: relative;display: inline-block;line-height: 22px;overflow: hidden;font-size: 14px;font-weight: 400;white-space: nowrap;text-align: center;border: 1px solid transparent;box-shadow: 0 2px #00000004;border-radius: 2px;border-color: #d9d9d9;color: #000000d9;background: #fff;cursor: pointer;pointer-events: auto;} 

index.htmlbody标签中加入

<r-button >默认按钮</r-button> 

pnpm dev执行项目,查看页面和控制台,是否正确生效

至此,最简单的默认按钮开发完成

2.水波纹特效

(1). 获取点击的位置

由于水波纹特效需要从点击的中心往周围扩散,所以需要获取当前点击的位置。 通过mousedown方法,获取点击的坐标,设置在元素的style属性中。利用css3中的变量,传递到shadow DOM中的样式。

// components/button/index.ts
// 写在CustomElement类中mousedown = (event: MouseEvent) => {const { left, top } = this.getBoundingClientRect();this.style.setProperty("--ran-x", event.clientX - left + "px");this.style.setProperty("--ran-y", event.clientY - top + "px");};connectedCallback() {this._btn.addEventListener("mousedown", this.mousedown);} 

点击的时候,可以看到页面效果

(2). 编写less样式

:host {position: relative; // 父元素需要设置相对定位// ...省略部分样式&::after {content: "";display: block;width: 100%;height: 100%;// 设置该元素的位置,搭配translate(-50%, -50%)居中position: absolute; left: var(--ran-x, 0); top: var(--ran-y, 0); transform: translate(-50%, -50%) scale(10);pointer-events: none;// 设置背景颜色background-image: radial-gradient(circle, #1890ff 10%, transparent 10.01%);background-repeat: no-repeat;opacity: 0;// 设置动画,搭配transform放大十倍效果,颜色由深到浅transition: transform 0.3s, opacity 0.8s; }
} 

(3).增加移除监听

// components/button/index.ts
// 写在CustomElement类中 mouseLeave = () => {this.style.removeProperty("--ran-x");this.style.removeProperty("--ran-y");}; connectedCallback() {this._btn.addEventListener("mousedown", this.mousedown);this._btn.addEventListener("mouseleave", this.mouseLeave);} 

3.增加按钮typedisabled属性

(1).type属性

希望按钮有四种类型

由于是通过元素的type属性来判断类型,所以首先要获取元素的type属性,且需要监听元素的type属性变化

// components/button/index.tsclass CustomElement extends HTMLElement {static get observedAttributes() {return ["type"];}constructor() {// ...}// 获取元素上的type属性get type() {return this.getAttribute('type')}// 设置元素上type属性set type(value) {if (value) {this.setAttribute('type', value)}}attributeChangedCallback(name: string, oldValue: string, newValue: string) {if(name === 'type'){// 这里写上当触发元素type属性变化是,需要进行的操作// 比如新设置的type属性和旧的type属性不一样,进行设置// 目前type属性只和样式有关,所以这些不是必须的,可以不用进行设置if (newValue && newValue !== oldValue ) {this.setAttribute('type', newValue)}}}} 

其实目前type属性只和样式有关,所以这不是必须的,可以不用进行设置。但后续的复杂属性,可以这样进行操作。本次我们更关心样式,根据不同的属性设置不同的样式,可以通过css来判断

 &([type="primary"]) {border-color: #1890ff;background-color: #1890ff;color: #fff;}&([type="warning"]) {border-color: #ff4d4f;background-color: #ff4d4f;color: #fff;}&([type="text"]) {border: none;} 

hover的时候,边框颜色会发生变化,且不同类型的按钮,边框变化颜色也不同

 &(:hover) {border-color: #1890ff;color: #1890ff;} &([type="primary"]:hover) {background-color: #40a9ff;color: #fff;} &([type="warning"]:hover) {border-color: #ff4d4f;background-color: #ff4d4f;color: #fff;} 

不同类型的按钮,点击触发的水波纹特效颜色不同,所以这里只需要设置颜色的区别

&([type="primary"]::after {background-image: radial-gradient(circle, #fff 10%, transparent 10.01%);
}
&([type="warning"]::after {background-image: radial-gradient(circle, #fff 10%, transparent 10.01%);
} 

(2).disabled属性

disabled的属性和type属性的开发过程差不多,区别在于,disabled属性是可以不需要值的,比如

 <r-button type="primary" disabled>disable按钮</r-button> 

但同时也需要兼容下有值的情况

  • disabled设置true,或者直接给元素添加disabled属性,或者给disabled属性设置任意不等于false的值,都算添加成功。
  • disabled设置false"false",undefined,null,就算是取消按钮的disabled。比如
 <r-button type="primary" disabled="false">disable按钮</r-button>
 <r-button type="primary" disabled="undefined">disable按钮</r-button> 

所以这一块我们可以抽取出来

1.封装一个判断方法
2.统一其他组件disabled功能的判断逻辑

我们先在utils中编写统一判断disabledfalse的情况

// utils/index.ts
export const falseList = [false, "false", null, undefined];
/**
 * @description: 判断这个元素上是否有disabled属性
 * @param {Element} element
 * @return {*}
 */
export const isDisabled = (element: Element) => {const status = element.hasAttribute("disabled");const value = element.getAttribute("disabled");if (status && !falseList.includes(value)) return true;return false;
}; 

在元素上设置disabled和获取disabled的值

// components/button/index.tsget disabled() {return isDisabled(this)}set disabled(value: boolean | string | undefined | null) {if (!value || value === "false") {this.removeAttribute("disabled");} else {this.setAttribute("disabled", '');}} 

逻辑部分写完,接下来是对disabled的样式处理

// components/button/index.less
&([disabled]) {cursor: not-allowed;pointer-events: all;opacity: 0.6;} 

由于disabledtype属性是可以并存的,所以这里也需要做一些兼容处理

// components/button/index.less

 &([type="primary"]:not([disabled]))::after {background-image: radial-gradient(circle, #fff 10%, transparent 10.01%);}&([type="primary"]:not([disabled]):hover) {background-color: #40a9ff;color: #fff;} 

对于水波纹特效,disabled的时候也肯定没有了

 mousedown = (event: MouseEvent) => {if (!this.disabled || this.disabled === 'false') {const { left, top } = this.getBoundingClientRect();this.style.setProperty("--ran-x", event.clientX - left + "px");this.style.setProperty("--ran-y", event.clientY - top + "px");}}; 

最后

整理了75个JS高频面试题,并给出了答案和解析,基本上可以保证你能应付面试官关于JS的提问。



有需要的小伙伴,可以点击下方卡片领取,无偿分享

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

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

相关文章

新C++(6):继承那些事儿

"你在酒杯还未干的时间里&#xff0c;收藏这份情谊"一、回顾继承什么是继承&#xff1f;继承是面向对象编程语言的三大特征之一。通过继承机制&#xff0c;面向对象的程序设计可以很大限度地对代码进行复用。它允许程序员在保持原有类特性的基础上进行扩展&#xff0…

scrapy_redis概念作用和流程

scrapy_redis概念作用和流程 学习目标 了解 分布式的概念及特点了解 scarpy_redis的概念了解 scrapy_redis的作用了解 scrapy_redis的工作流程 在前面scrapy框架中我们已经能够使用框架实现爬虫爬取网站数据,如果当前网站的数据比较庞大, 我们就需要使用分布式来更快的爬取数…

制药行业中各种化合物的净化,大孔吸附净化氨基酸

基于吸附功能的聚苯乙烯特种树脂 Tulsimer ADS-600 是一款没有离子官能基的&#xff0c;由交联聚苯乙烯合成的功能强大的吸附型树脂。 Tulsimer ADS-600 主要应用于水溶液中吸附酚及其化合物&#xff0c;氯代烃等含氯物质&#xff0c;表面活性剂&#xff0c;氨基酸&#…

【C++修炼之路】C++入门(中)—— 函数重载和引用

&#x1f451;作者主页&#xff1a;安 度 因 &#x1f3e0;学习社区&#xff1a;StackFrame &#x1f4d6;专栏链接&#xff1a;C修炼之路 文章目录一、前言二、函数重载1、重载规则2、函数名修饰规则三、引用1、区分2、本质3、特性4、应用a、做参数b、做返回值5、效率比较6、常…

windows安装cnpm

文章目录1 cnpm简介2 cnpm 安装步骤1 cnpm简介 npm下载速度比较慢&#xff0c;可以通过cnpm下载node包 2 cnpm 安装步骤 找到nodejs的安装路径&#xff1a; 使用nvm安装和管理node 直接安装node的方式 直接通过安装包安装node的方式&#xff0c;node会被安装在某个实际目录下…

docker pull nginx

取最新版的 Nginx 镜像 docker pull nginx:latest 查看本地镜像 使用以下命令来查看是否已安装了 nginx&#xff1a; $ docker images 运行容器 安装完成后&#xff0c;我们可以使用以下命令来运行 nginx 容器&#xff1a; $ docker run --name nginx-door -p 8080:80 -…

VS code的使用指南

VS code的使用指南 VS code的安装与下载 VS Code的安装地址的 在下面选择版本的位置进行安装&#xff08;一般选择Stable进行安装&#xff0c;这个软件是跨系统的安装与设置相关的内容信息&#xff09;。 插件安装 直接点击这个位置,选择自己需要的相应的插件 这些都是常用的…

软件工程(四)——结构化设计、模块独立性、面向对象设计、软件测试与维护

目录 一、界面设计 二、结构化设计 1.概要设计 2模块独立性 三、面向对象设计 1.面向对象设计的五大基本原则(SOLID)和其他5个原则 2.设计模式 三、软件测试与维护 1.白盒测试和黑盒测试 2.测试的阶段 3.软件维护阶段 四、系统演化策略 一、界面设计 人机界面设计&…

50条必背JAVA知识点(一)

1.编写&#xff1a;编写的Java代码保存在以“.java”结尾的源文件中。 2.编译&#xff1a;使用javac.exe命令编译java源文件&#xff0c;生成字节码文件。格式&#xff1a;javac 源文件名.java 3.运行&#xff1a;使用java.exe命令解释运行字节码文件。格式&#xff1a;java …

深入浅出PyTorch-PyTorch的主要组成模块

目录1.基本配置2.数据读入3.模型构建3.1神经网络的构造3.2神经网络中常见的层常见网络层的构造常见的网络层3.3模型示例卷积神经网络&#xff08;LeNet&#xff09;深度卷积神经网络&#xff08;AlexNet&#xff09;4.模型初始化5.损失函数6.训练和评估深度学习和机器学习在流程…

python小游戏——塔防小游戏代码开源

♥️作者&#xff1a;小刘在这里 ♥️每天分享云计算网络运维课堂笔记&#xff0c;努力不一定有收获&#xff0c;但一定会有收获加油&#xff01;一起努力&#xff0c;共赴美好人生&#xff01; ♥️夕阳下&#xff0c;是最美的&#xff0c;绽放&#xff0c;愿所有的美好&#…

TCP/IP网络编程——优雅的断开套接字的连接

完整版文章请参考&#xff1a; TCP/IP网络编程完整版文章 文章目录第 7 章 优雅的断开套接字的连接7.1 基于 TCP 的半关闭7.1.1 单方面断开连接带来的问题7.1.2 套接字和流&#xff08;Stream&#xff09;7.1.3 针对优雅断开的 shutdown 函数7.1.4 为何要半关闭7.1.5 基于半关闭…

AST入门与反混淆初体验

文章目录1.什么是AST&#xff1f;2. AST反混淆的目的3. babel库安装4. 直观的理解AST5.如何用AST解混淆&#xff1f;思路是什么&#xff1f;6. babel库的学习7. AST反混淆初体验-常量折叠1.什么是AST&#xff1f; ​ 在计算机科学中&#xff0c;抽象语法树&#xff08;Abstrac…

【服务器数据恢复】raid5中3块磁盘先后掉线的数据恢复案例

服务器数据恢复环境&故障&#xff1a; 某单位同友存储设备&#xff0c;该存储有大于5台的虚拟机&#xff0c;其中有3台linux系统虚拟机存储重要数据。存储设备中组建的raid5由于未知原因崩溃导致存储无法启动。 存储结构&#xff1a; 服务器数据恢复过程&#xff1a; 1、对…

C语言进阶——字符函数和字符串函数(上)

目录 一、前言 二、正文 1.求字符串长度 ♥strlen 2.长度不受限制的字符串函数 ♥strcpy ♥strcat ♥strcmp 三、结语 一、前言 一日不见&#xff0c;如隔三秋&#xff1b;几日不见&#xff0c;甚是想念。猜想小伙伴们在平常进行有关字符的练习时遇到有关字符的操作却无从下手…

GEC6818 移植 rtl8723bu wifi驱动

文章目录1. 配置内核2、RTL8723BU 模块驱动编译2.1 下载解压2.2 配置编译3. openssl 移植3.1 下载解压3.2 配置3.3 编译安装4. libnl 移植4.1 下载解压4.2 进入源码目录并配置4.3 编译安装5. wpa_supplicant 移植5.1 解压源码5.2 配置5.3 make编译6. 启动wifi网卡6.1 配置WiFi连…

Python调用Go语言编译的动态链接库(CGO)【待续】

CGO C 语言作为一个通用语言&#xff0c;很多库会选择提供一个 C 兼容的 API&#xff0c;然后用其他不同的编程语言实现。Go 语言通过自带的一个叫 CGO 的工具来支持 C 语言函数调用&#xff0c;同时我们可以用 Go 语言导出 C 动态库接口给其它语言使用。 hello world程序 p…

【定时同步系列11】各种开环定时同步算法与MCRB性能对比的MATLAB仿真

重要声明:为防止爬虫和盗版贩卖,文章中的核心代码和数据集可凭【CSDN订阅截图或公z号付费截图】私信免费领取,一律不认其他渠道付费截图! 引言 开环定时同步包括内插控制、定时误差估计等环节,其中内插控制在之前的博客里有详细的描述,请翻阅之前的博客! 【定时同步系…

Python编程技巧分享:6 个必知必会高效 Python

编写更好的Python 代码需要遵循Python 社区制定的最佳实践和指南。遵守这些标准可以使您的代码更具可读性、可维护性和效率。 本文将展示一些技巧&#xff0c;帮助您编写更好的 Python 代码 遵循 PEP 8 风格指南 PEP 8 是 Python 代码的官方风格指南。它提供了一组用于格式化…

Maven parent多项目打包找不到reversion变量问题

项目结构&#xff1a;packagetest&#xff1a;顶级父级c1&#xff1a;子项目&#xff08;web项目&#xff09;c2&#xff1a;子项目(jar包)c1依赖c2的jar包。在父级maven中deploy成功&#xff0c;package也成功&#xff0c;私服上有都有包了。但是在c1上package的时候&#xff…