Web Component技术
严重警告
在 Chrome 版本 76.0.3809.132(正式版本)(64 位)中测试发现,customElements.define()必须在 js 文件中调用,且引用此 js 文件时必须在script标签上添加defer属性,否则this.getAttribute('属性名称')无法获取到值。
基本概况
Web Components 是一套不同的技术,允许您创建可重用的定制元素(它们的功能封装在您的代码之外)并且在您的 web 应用中使用它们。其中,Web Components由三项主要技术组成,分别为Custom elements(自定义元素)、Shadow DOM(影子 DOM)、HTML templates(HTML 模板)等。
- Web Components: https://developer.mozilla.org/zh-CN/docs/Web/Web_Components
- GitHub: https://github.com/mdn/web-components-examples
Web Components特征
- 封装性:将标记结构、样式和行为隐藏起来,并与页面上的其他代码相隔离,保证不同的部分不会混在一起,可使代码更加干净、整洁。
- Shadow DOM 接口:将一个隐藏的、独立的 DOM 附加到一个元素上。
独立自定义元素
继承自 HTML 元素基类 HTMLElement。
示例一:不内嵌元素
<cusElement></cusElement>
<script type="text/javascript">
class cusElement extends HTMLElement {
// 必须是一个包含元素需要变更通知的所有属性名称的数组
static observedAttributes = ["color", "size"];
constructor() {
super();
// 注意:一旦创建了这个对象,则自定义元素的容器大小就失效,需要重新设置大小
this.shadow = this.attachShadow({ mode: 'closed' });
this.init();
}
init() {
// 无效,在constructor构造函数中调用无效
const color = this.getAttribute("color");
console.log("颜色: ", color);
}
// 每当元素添加到文档中时调用
connectedCallback() {
console.log("自定义元素添加至页面。");
// 获取 自定义元素 外的其他元素
console.log(document.querySelector("#cusElementId"));
console.log(document.querySelector("body"));
console.log(document.querySelector('[id^="GalleryStatus"]'));
/**
* 特别注意
* 这种通过this.querySelector获取自定义元素内部的元素,只能通过<script src="./temp.js" defer></script>外部引入脚本的方式,才能生效。
*/
console.log(this.querySelector("h1"));
// 获取属性
const color = this.getAttribute("color");
console.log("颜色: ", color);
window.setTimeout(() => {
// 设置属性
this.setAttribute("color", "#0ff");
});
}
// 每当元素从文档中移除时调用
disconnectedCallback() {
console.log("自定义元素从页面中移除。");
}
// 每当元素被移动到新文档中时调用
adoptedCallback() {
console.log("自定义元素移动至新页面。");
}
// 在属性更改、添加、移除或替换时调用
attributeChangedCallback(name, oldValue, newValue) {
console.log(
`属性 ${name} 的旧值: ${oldValue} 已经出现更新,新值: ${newValue}`
);
}
}
const EL_KEY = "cus-element";
if (!customElements.get(EL_KEY)) {
customElements.define(EL_KEY, cusElement);
}
</script>
示例二:内嵌元素
<cus-element id="cusElementId" color="#333" size="10">
<h1>Web Component</h1>
<p>
Web Component
是一套不同的技术,允许你创建可重用的定制元素(它们的功能封装在你的代码之外)并且在你的
web 应用中使用它们。
</p>
<h3>独立自定义元素</h3>
<p>继承自 HTML 元素基类 HTMLElement。</p>
</cus-element>
<script type="text/javascript">
class cusElement extends HTMLElement {
// 必须是一个包含元素需要变更通知的所有属性名称的数组
static observedAttributes = ["color", "size"];
constructor() {
super();
this.init();
}
init() {
// 无效,在constructor构造函数中调用无效
const color = this.getAttribute("color");
console.log("颜色: ", color);
}
// 每当元素添加到文档中时调用
connectedCallback() {
console.log("自定义元素添加至页面。");
// 获取属性
const color = this.getAttribute("color");
console.log("颜色: ", color);
window.setTimeout(() => {
// 设置属性
this.setAttribute("color", "#0ff");
});
}
// 每当元素从文档中移除时调用
disconnectedCallback() {
console.log("自定义元素从页面中移除。");
}
// 每当元素被移动到新文档中时调用
adoptedCallback() {
console.log("自定义元素移动至新页面。");
}
// 在属性更改、添加、移除或替换时调用
attributeChangedCallback(name, oldValue, newValue) {
console.log(
`属性 ${name} 的旧值: ${oldValue} 已经出现更新,新值: ${newValue}`
);
}
}
const EL_KEY = "cus-element";
customElements.define(EL_KEY, cusElement);
</script>
实现案例
<cusElement></cusElement>
<script>
(function (global, factory) {
'use strict';
factory();
}(this, function () {
'use strict';
/**
* 公共函数
*/
function appendHTML(parentEl, html) { var divTemp = document.createElement("div"); var nodes = null; var fragment = document.createDocumentFragment(); divTemp.innerHTML = html; nodes = divTemp.childNodes; for (var i = 0; i < nodes.length; i++) { fragment.appendChild(nodes[i].cloneNode(true)) } parentEl.appendChild(fragment); nodes = null; fragment = null } function trim(string) { return (string || "").replace(/^[\s\uFEFF]+|[\s\uFEFF]+$/g, "") } function hasClass(el, cls) { if (!el || !cls) return false; if (cls.indexOf(' ') !== -1) { throw new Error('className should not contain space.'); } if (el.classList) { return el.classList.contains(cls) } if (el.className == '') { return false } return el.className.indexOf(cls) != -1 } function addClass(el, cls) { if (!el) return; var curClass = el.className; var classes = (cls || '').split(' '); for (var i = 0, j = classes.length; i < j; i++) { var clsName = classes[i]; if (!clsName) continue; if (el.classList) { el.classList.add(clsName) } else { if (!hasClass(el, clsName)) { curClass += ' ' + clsName } } } if (!el.classList) { el.className = curClass } }; function removeClass(el, cls) { if (!el || !cls) return; var classes = cls.split(' '); var curClass = ' ' + el.className + ' '; for (var i = 0, j = classes.length; i < j; i++) { var clsName = classes[i]; if (!clsName) continue; if (el.classList) { el.classList.remove(clsName) } else { if (hasClass(el, clsName)) { curClass = curClass.replace(' ' + clsName + ' ', ' ') } } } if (!el.classList) { el.className = trim(curClass) } }; function toggleClass(el, cls) { if (!el || !cls) return; if (hasClass(el, cls)) { removeClass(el, cls); return } addClass(el, cls) }
/**
* 公共请求参数
*/
const REQUEST_PARAMS = {
p_channel: "",
p_module: "module"
};
// 生产环境
const PROD_BASE_URL = "https://xxx.xxx.com/ares/v1/";
// 测试环境
const DEVT_BASE_URL = "https://dev-xxx.xxx.com/ares/v1/";
const COMMON_DATA = {
baseURL: ""
};
// 注册组件键名
const EL_KEY = "cus-el-carousel-display";
if (!customElements.get(EL_KEY)) {
customElements.define(EL_KEY,
class cusElement extends HTMLElement {
static observedAttributes = [];
constructor() {
super();
// shadow DOM 设置'closed',即不可以从外部获取 Shadow DOM
// 注意,创建了该元素,就不可以在该组件内嵌任何HTML片段,会无效
this.shadow = this.attachShadow({mode: 'closed'});
this.renderStyle();
this.init();
}
/**
* 渲染样式
*/
renderStyle() {
const style = document.createElement('style');
style.textContent = `
* {-webkit-tap-highlight-color: rgba(0, 0, 0, 0);}
* {-webkit-box-sizing: border-box;box-sizing: border-box;}
.${EL_KEY} {}
`;
this.shadow.appendChild(style);
}
init(options = {}) {
this.initGlobal();
this.renderUI();
this.activateEvent();
this.resizeEvent();
}
initGlobal(options = {}) {
const debugAtrr = options.debug || this.getAttribute('debug');
const channelAttr = options.channel || this.getAttribute('channel');
// 调试模式
this.debug = debugAtrr === "true" ? true : false;
this.channel = channelAttr || "";
}
/**
* 渲染UI
*/
renderUI() {
const mainUI = `
<div class="${EL_KEY} ${this.debug ? 'debug' : ''}">
</div>
`;
// 清空旧内容
const mainEl = this.shadow.querySelector(`.${EL_KEY}`);
mainEl?.remove();
// 追加新内容
appendHTML(this.shadow, mainUI);
}
/**
* 激活事件
*/
activateEvent() {
this.closeModel();
}
// 关闭事件
closeModel() {
const closeEl = this.shadow.querySelector("div");
console.info('closeEl: ', closeEl);
}
/**
* 窗口尺寸变化
*/
resizeEvent() {
const _this = this;
window.onresize = function () {
_this.renderUI();
_this.activateEvent();
}
}
connectedCallback() {
this.setAttribute("style", "position: relative; display: block; width: 100%; height: 100%");
}
/**
* 监听外部传递属性变化
*/
attributeChangedCallback(name, oldValue, newValue) {
const value = newValue || oldValue;
if (name === "") {
//
}
}
}
);
}
/**
* 业务系统
*/
function initModuleName(options = {}) {
const {
// 渠道类型
channel = "",
// 调试模式
debug = false,
// 环境,默认为测试环境,"development"-测试环境 "production"-生产环境
env = "development"
} = options || {};
const baseURL = env === "production" ? PROD_BASE_URL : DEVT_BASE_URL;
if (!channel) throw new Error("channel参数必填,不能为空");
REQUEST_PARAMS.p_channel = channel;
// 启动程序
}
return { initModuleName };
}));
</script>
自定义内置元素
继承自标准的 HTML 元素,例如 HTMLImageElement 或 HTMLParagraphElement。它们的实现定义了标准元素的行为。
<div is="cus-element"></div>
<script>
class cusElement extends HTMLElement {
constructor() {
super();
}
}
if (!customElements.get("cus-element")) {
customElements.define('cus-element', cusElement, {
extends: "div"
});
}
</script>
基本语法
Custom elements(自定义元素)
一组 JavaScript API,允许您定义 custom elements 及其行为,然后可以在您的用户界面中按照需要使用它们。
customElements.get
指定名字的自定义元素的构造函数,如果没有使用该名称的自定义元素定义,则为undefined。
const EL_KEY = "cus-element";
if (!customElements.get(EL_KEY)) {
customElements.define(EL_KEY, cusElement);
}
可以被挂载的 shadow DOM 元素
- 任何带有有效的名称且可独立存在的(autonomous)自定义元素。
<article><aside><blockquote><body><div><footer><h1><h2><h3><h4><h5><h6><header><main><nav><p><section><span>
。
注意事项
不是每一种类型的元素都可以附加到 shadow root(影子根)下面。出于安全考虑,一些元素不能使用 shadow DOM(例如<a>
),以及许多其他的元素。
Shadow DOM(影子 DOM)
Shadow DOM 接口,可以将标记结构、样式和行为隐藏起来,并与页面上的其他代码相隔离,保证不同的部分不会混在一起,可使代码更加干净、整洁。
HTML templates(HTML 模板)
<template>
和<slot>
元素使您可以编写不在呈现页面中显示的标记模板。然后它们可以作为自定义元素结构的基础被多次重用。
- 是一种用于保存客户端内容机制,该内容在加载页面时不会呈现,但随后可以 (原文为 may be) 在运行时使用 JavaScript 实例化。
- 解析器在加载页面时确实会处理
<template>
元素的内容,但这样做只是为了确保这些内容有效;但元素内容不会被渲染。 <slot>
元素是 Web 组件内的一个占位符。该占位符可以在后期使用自己的标记语言填充,这样您就可以创建单独的 DOM 树,并将它与其它的组件组合在一起。使用槽 (slots) 可以添加灵活度。- templates模板中style样式只给它添加到一个标准的 DOM 中是不起作用,而是在模板内部添加style才能生效。
实现web component基本方法
- 创建一个类或函数来指定 web 组件的功能。
- 使用 CustomElementRegistry.define() 方法注册您的新自定义元素 ,并向其传递要定义的元素名称、指定元素功能的类、以及可选的其所继承自的元素。
- 如果需要的话,使用Element.attachShadow() 方法将一个 shadow DOM 附加到自定义元素上。使用通常的 DOM 方法向 shadow DOM 中添加子元素、事件监听器等等。
- 如果需要的话,使用
<template>
和<slot>
定义一个 HTML 模板。再次使用常规 DOM 方法克隆模板并将其附加到您的 shadow DOM 中。 - 在页面任何您喜欢的位置使用自定义元素,就像使用常规 HTML 元素那样。
自定义标签与Shadow DOM
<cus-element></cus-element>
customElements.define
customElements.define(
name, // 表示所创建的元素名称的符合 DOMString 标准的字符串。注意,custom element 的名称不能是单个单词,且其中必须要有短横线。
ComponentName, // 用于定义元素行为的 类 。
options // 一个包含 extends 属性的配置对象,是可选参数。它指定了所创建的元素继承自哪个内置元素,可以继承任何内置元素。
);
<template>
模板
正常使用<template id="template">
<style>
.template-wrapper {
position: relative;
}
</style>
<div class="template-wrapper">内容模板元素</div>
<slot name="slotcontent"></slot>
</template>
<script type="text/javascript">
const template = document.getElementById('template');
const templateContent = template.content;
document.body.appendChild(templateContent);
</script>
在 Web Components 中使用模板
<template id="template">
<style>
.template-wrapper {
position: relative;
}
</style>
<div class="template-wrapper">内容模板元素</div>
<slot name="slotcontent"></slot>
</template>
<cus-element>
<div slot='slotcontent'>使用槽 (slots) 添加灵活度</div>
</cus-element>
<script type="text/javascript">
class cusElement extends HTMLElement {
constructor() {
super();
this.init();
}
init() {
// shadow DOM 设置'closed',即不可以从外部获取 Shadow DOM
const shadow = this.attachShadow({mode: 'closed'});
const template = document.getElementById('template');
const templateContent = template.content;
// 使用Node.cloneNode() 方法添加了模板的拷贝到阴影的根结点上。
shadow.appendChild(templateContent.cloneNode(true));
}
}
customElements.define('cus-element', cusElement);
</script>
生命周期回调函数
connectedCallback
当 custom element 首次被插入文档 DOM 时,被调用。
if (!customElements.get("cus-element")) {
customElements.define(
'cus-element',
class cusElement extends HTMLElement {
constructor() {
super();
this.init();
}
init() {
//
}
connectedCallback() {
console.log("自定义元素添加至页面。");
}
}
);
}
disconnectedCallback
当 custom element 从文档 DOM 中删除时,被调用。
if (!customElements.get("cus-element")) {
customElements.define(
'cus-element',
class cusElement extends HTMLElement {
constructor() {
super();
this.init();
}
init() {
//
}
disconnectedCallback() {
console.log("自定义元素从页面中移除。");
}
}
);
}
adoptedCallback
当 custom element 被移动到新的文档时,被调用。
if (!customElements.get("cus-element")) {
customElements.define(
'cus-element',
class cusElement extends HTMLElement {
constructor() {
super();
this.init();
}
init() {
//
}
adoptedCallback() {
console.log("自定义元素移动至新页面。");
}
}
);
}
attributeChangedCallback
当 custom element 增加、删除、修改自身属性时,被调用。
配置步骤
- 步骤一:将需要变更通知的所有属性名称赋值给一个名为 observedAttributes 的静态属性。
- 步骤二:attributeChangedCallback() 生命周期回调的实现。
代码示例
if (!customElements.get("cus-element")) {
customElements.define(
'cus-element',
class cusElement extends HTMLElement {
static observedAttributes = ["name", "title"];
constructor() {
super();
this.init();
}
init() {
//
}
attributeChangedCallback(name, oldValue, newValue) {
const value = newValue || oldValue;
console.log(`属性 ${name} 已变更。`, oldValue, newValue);
}
}
);
}
通信实现机制
CustomEvent自定义事件
// 发送数据
const event = new CustomEvent("ysun-turntable-event", {
detail: {
type: "render-ui",
data: data
},
bubbles: true,
composed: true
});
document.dispatchEvent(event);
// 监听外部事件
document.addEventListener(
"ysun-turntable-event",
function (e) {
const { type, data } = e.detail || {};
// 渲染UI
if (type === "render-ui") {
const { list } = data || {};
}
// 旋转
if (type === "start-rorate") {
//
}
},
false
);
postMessage
事件(Event)处理机制
indicatorEvent() {
const _this = this;
const indicatorEls = this.shadow.querySelectorAll(".carousel-indicator-item");
const indicatorElsList = Array.from(indicatorEls);
for (let i = 0; i < indicatorElsList.length; i++) {
const el = indicatorElsList[i];
el?.addEventListener("click", function (e) {
_this.indicatorModel(i);
}, false);
}
}
Omi
Omi 是腾讯开源的前端跨框架跨平台的框架。
Stencil
Stencil 是用于生成 Web Components 的编译器
Lit
- 官网: https://lit.dev/
- GitHub: https://github.com/lit/lit/
Spectrum
Web Components 库:Spectrum
Spectrum Web Components 具有以下特点:
默认支持无障碍访问:开发时考虑到现有和新兴浏览器规范,以支持辅助技术。 轻量级:使用 Lit Element 实现,开销最小。 基于标准:基于 Web Components 标准,如自定义元素和 Shadow DOM 构建。 框架无关:由于浏览器级别的支持,可以与任何框架一起使用。
- 官网: https://opensource.adobe.com/spectrum-web-components/index.html
- GitHub: https://github.com/adobe/spectrum-web-components
Slim.js
Slim.js 是一个开源的轻量级 Web Components 库,它为组件提供数据绑定和扩展能力,使用 es6 原生类继承。
Polymer
Polymer 是 Google 推出的 Web Components 库
hybrids
hybrids 是一个 JavaScript UI 框架,用于创建功能齐全的 Web 应用程序、组件库或具有独特的混合声明性和功能性架构的单个 Web Components。
direflow
direflow 是一个 React组件 + web component +web componen t属性变化重新挂载 React 组件的 web component框架。
LitElement
LitElement 是一个简单的基类,用于使用 lit-html 创建快速、轻量级的 Web Components。
X-Tag
X-Tag 是微软推出的开源库,支持 Web Components 规范,兼容Web Components。