2016年12月5日更新:在评论中进行了一些讨论之后,撰写了第二篇文章以解决该文章的缺点- 如何使可访问的Web组件 。 请确保也阅读此内容。
本文由Ryan Lewis同行评审。 感谢所有SitePoint的同行评审人员使SitePoint内容达到最佳状态!
Web应用程序变得越来越复杂,并且需要大量的标记,脚本和样式。 为了管理和维护数百KB的HTML,JS和CSS,我们尝试将应用程序拆分为可重用的组件。 我们努力封装组件,防止样式冲突和脚本干扰。
最后,组件源代码分布在多个文件之间:标记文件,脚本文件和样式表。 我们可能会遇到的另一个问题是,长标记因div
和span
混乱。 这种代码表现力很弱,也很难维护。 为了解决并尝试解决所有这些问题,W3C引入了Web组件。
在本文中,我将解释什么是Web组件以及如何自己构建Web组件。
认识Web组件
Web组件解决了引言中讨论的所有这些问题。 使用Web组件,我们可以链接包含组件实现的单个HTML文件,并将其与自定义HTML元素一起在页面上使用。 它们简化了组件的创建,增强了封装,并使标记更具表现力。
Web组件通过一系列规范进行定义:
- 自定义元素 :允许为组件注册自定义的有意义的HTML元素
- HTML模板 :定义组件的标记
- Shadow DOM :封装组件的内部并将其从使用它的页面隐藏
- HTML Imports :提供将组件包含到目标页面的功能。
描述了什么是Web组件之后,让我们看看它们的实际作用。
如何构建可用于生产的Web组件
在本节中,我们将构建一个有用的多选小部件,该小部件可以在生产中使用。 结果可以在此演示页面上找到,完整的源代码可以在GitHub上找到 。
要求
首先,让我们为多选小部件定义一些要求。
标记应具有以下结构:
<x-multiselect placeholder="Select Item">
<li value="1" selected>Item 1</li>
<li value="2">Item 2</li>
<li value="3" selected>Item 3</li>
</x-multiselect>
定制元素<x-multiselect>
具有一个placeholder
属性,用于定义空的multiselect的占位符。 使用支持value
和selected
属性的<li>
元素定义项目。
多重selectedItems
应该具有selectedItems
API方法,该方法返回所选项目的数组。
// returns an array of values, e.g. [1, 3]
var selectedItems = multiselect.selectedItems();
此外,小部件应在每次change
所选项目时触发事件change
。
multiselect.addEventListener('change', function() {
// print selected items to console
console.log('Selected items:', this.selectedItems());
});
最后,该小部件应在所有现代浏览器中都可以使用。
模板
我们开始创建multiselect.html
文件,该文件将包含组件的所有源代码:HTML标记,CSS样式和JS代码。
HTML模板允许我们在特殊的HTML元素<template>
定义组件的<template>
。 这是我们的多选模板:
<template id="multiselectTemplate">
<style>
/* component styles */
</style>
<!-- component markup -->
<div class="multiselect">
<div class="multiselect-field"></div>
<div class="multiselect-popup">
<ul class="multiselect-list">
<content select="li"></content>
</ul>
</div>
</div>
</template>
组件标记包含多重选择的字段和带有项目列表的弹出窗口。 我们希望多选从用户标记中获取项目。 我们可以使用新的HTML元素<content>
来做到这一点( 您可以在MDN上找到有关content
元素的更多信息 )。 它定义了从影子主机(用户标记中的组件声明)到影子DOM(封装的组件标记)的标记的插入点。
select
属性接受CSS选择器并定义要从影子主机中选择的元素。 在我们的例子中,我们要采用所有<li>
元素并设置select="li"
。
创建组件
现在,让我们创建一个组件并注册一个自定义HTML元素。 将以下创建脚本添加到multiselect.html
文件:
<script>
// 1. find template
var ownerDocument = document.currentScript.ownerDocument;
var template = ownerDocument.querySelector('#multiselectTemplate');
// 2. create component object with the specified prototype
var multiselectPrototype = Object.create(HTMLElement.prototype);
// 3. define createdCallback
multiselectPrototype.createdCallback = function() {
var root = this.createShadowRoot();
var content = document.importNode(template.content, true);
root.appendChild(content);
};
// 4. register custom element
document.registerElement('x-multiselect', {
prototype: multiselectPrototype
});
</script>
Web组件的创建包括四个步骤:
- 在所有者文档中找到一个模板。
- 使用指定的原型对象创建一个新对象。 在这种情况下,我们将从现有的HTML元素继承,但是可以扩展任何可用的元素。
- 定义创建组件时调用的
createdCallback
。 在这里,我们为组件创建一个影子根,并在其中附加模板的内容。 - 使用
document.registerElement
方法注册组件的自定义元素。
要了解有关创建自定义元素的更多信息,建议您阅读Eric Bidelman的指南 。
渲染多选字段
下一步是根据选定的项目渲染多选字段。
入口点是createdCallback
方法。 让我们定义两个方法, init
和render
:
multiselectPrototype.createdCallback = function() {
this.init();
this.render();
};
init
方法创建一个影子根,并找到所有内部组成部分(字段,弹出窗口和列表):
multiselectPrototype.init = function() {
// create shadow root
this._root = this.createRootElement();
// init component parts
this._field = this._root.querySelector('.multiselect-field');
this._popup = this._root.querySelector('.multiselect-popup');
this._list = this._root.querySelector('.multiselect-list');
};
multiselectPrototype.createRootElement = function() {
var root = this.createShadowRoot();
var content = document.importNode(template.content, true);
root.appendChild(content);
return root;
};
render
方法执行实际的渲染。 因此,它将调用refreshField
方法,该方法遍历所选项目并为每个所选项目创建标签:
multiselectPrototype.render = function() {
this.refreshField();
};
multiselectPrototype.refreshField = function() {
// clear content of the field
this._field.innerHTML = '';
// find selected items
var selectedItems = this.querySelectorAll('li[selected]');
// create tags for selected items
for(var i = 0; i < selectedItems.length; i++) {
this._field.appendChild(this.createTag(selectedItems[i]));
}
};
multiselectPrototype.createTag = function(item) {
// create tag text element
var content = document.createElement('div');
content.className = 'multiselect-tag-text';
content.textContent = item.textContent;
// create item remove button
var removeButton = document.createElement('div');
removeButton.className = 'multiselect-tag-remove-button';
removeButton.addEventListener('click', this.removeTag.bind(this, tag, item));
// create tag element
var tag = document.createElement('div');
tag.className = 'multiselect-tag';
tag.appendChild(content);
tag.appendChild(removeButton);
return tag;
};
每个标签都有一个删除按钮。 删除按钮单击处理程序将从项目中删除选择并刷新multiselect字段:
multiselectPrototype.removeTag = function(tag, item, event) {
// unselect item
item.removeAttribute('selected');
// prevent event bubbling to avoid side-effects
event.stopPropagation();
// refresh multiselect field
this.refreshField();
};
打开弹出窗口并选择项目
当用户单击该字段时,我们应该显示弹出窗口。 当他/她单击列表项时,应将其标记为已选择,并且应隐藏弹出窗口。
为此,我们处理字段和项目列表上的单击。 让我们将attachHandlers
方法添加到render
:
multiselectPrototype.render = function() {
this.attachHandlers();
this.refreshField();
};
multiselectPrototype.attachHandlers = function() {
// attach click handlers to field and list
this._field.addEventListener('click', this.fieldClickHandler.bind(this));
this._list.addEventListener('click', this.listClickHandler.bind(this));
};
在字段单击处理程序中,我们切换弹出窗口的可见性:
multiselectPrototype.fieldClickHandler = function() {
this.togglePopup();
};
multiselectPrototype.togglePopup = function(show) {
show = (show !== undefined) ? show : !this._isOpened;
this._isOpened = show;
this._popup.style.display = this._isOpened ? 'block' : 'none';
};
在列表单击处理程序中,我们找到被单击的项目并将其标记为选中状态。 然后,我们隐藏弹出窗口并刷新多选字段:
multiselectPrototype.listClickHandler = function(event) {
// find clicked list item
var item = event.target;
while(item && item.tagName !== 'LI') {
item = item.parentNode;
}
// set selected state of clicked item
item.setAttribute('selected', 'selected');
// hide popup
this.togglePopup(false);
// refresh multiselect field
this.refreshField();
};
添加占位符属性
另一个多选功能是placeholder
属性。 当未选择任何项目时,用户可以指定要在字段中显示的文本。 为了完成此任务,让我们读取组件初始化时的属性值(在init
方法中):
multiselectPrototype.init = function() {
this.initOptions();
...
};
multiselectPrototype.initOptions = function() {
// save placeholder attribute value
this._options = {
placeholder: this.getAttribute("placeholder") || 'Select'
};
};
当未选择任何项目时, refreshField
方法将显示占位符:
multiselectPrototype.refreshField = function() {
this._field.innerHTML = '';
var selectedItems = this.querySelectorAll('li[selected]');
// show placeholder when no item selected
if(!selectedItems.length) {
this._field.appendChild(this.createPlaceholder());
return;
}
...
};
multiselectPrototype.createPlaceholder = function() {
// create placeholder element
var placeholder = document.createElement('div');
placeholder.className = 'multiselect-field-placeholder';
placeholder.textContent = this._options.placeholder;
return placeholder;
};
但这还不是故事的结局。 如果占位符属性值更改了怎么办? 我们需要处理并更新该字段。 在这里, attributeChangedCallback
回调很方便。 每次更改属性值时都会调用此回调。 在我们的例子中,我们保存一个新的占位符值并刷新多选字段:
multiselectPrototype.attributeChangedCallback = function(optionName, oldValue, newValue) {
this._options[optionName] = newValue;
this.refreshField();
};
添加selectedItems
方法
我们需要做的就是向组件原型添加一个方法。 selectedItems
方法的实现很简单–循环选中的项目并读取值。 如果项目没有值,则返回项目文本:
multiselectPrototype.selectedItems = function() {
var result = [];
// find selected items
var selectedItems = this.querySelectorAll('li[selected]');
// loop over selected items and read values or text content
for(var i = 0; i < selectedItems.length; i++) {
var selectedItem = selectedItems[i];
result.push(selectedItem.hasAttribute('value')
? selectedItem.getAttribute('value')
: selectedItem.textContent);
}
return result;
};
添加自定义事件
现在,让我们添加一次change
事件,该事件将在用户每次更改选择时触发。 要触发事件,我们需要创建一个CustomEvent
实例并调度它:
multiselectPrototype.fireChangeEvent = function() {
// create custom event instance
var event = new CustomEvent("change");
// dispatch event
this.dispatchEvent(event);
};
此时,我们需要在用户选择或取消选择项目时触发事件。 在列表单击处理程序中,我们仅在实际选择一个项目时触发该事件:
multiselectPrototype.listClickHandler = function(event) {
...
if(!item.hasAttribute('selected')) {
item.setAttribute('selected', 'selected');
this.fireChangeEvent();
this.refreshField();
}
...
};
在移除标签按钮处理程序中,由于未选择项目,我们还需要触发change
事件:
multiselectPrototype.removeTag = function(tag, item, event) {
...
this.fireChangeEvent();
this.refreshField();
};
造型
设置Shadow DOM的内部元素非常简单。 我们附加一些特殊的类,例如multiselect-field
或multiselect-popup
并为它们添加必要的CSS规则。
但是如何为列表项设置样式? 问题在于它们来自影子主机,不属于影子DOM。 特殊选择器::content
可以帮助我们。
以下是我们列表项的样式:
::content li {
padding: .5em 1em;
min-height: 1em;
list-style: none;
cursor: pointer;
}
::content li[selected] {
background: #f9f9f9;
}
Web Components引入了一些特殊的选择器, 您可以在此处找到有关它们的更多信息 。
用法
大! 我们的多选功能已完成,因此可以使用它了。 我们需要做的就是导入多选HTML文件,并将自定义元素添加到标记中:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<link rel="import" href="multiselect.html">
</head>
<body>
<x-multiselect placeholder="Select Value">
<li value="1" selected>Item 1</li>
<li value="2">Item 2</li>
<li value="3" selected>Item 3</li>
<li value="4">Item 4</li>
</x-multiselect>
</body>
</html>
让我们订阅change
事件,并在用户每次更改选择时将选择的项目打印到控制台:
<script>
var multiselect = document.querySelector('x-multiselect');
multiselect.addEventListener('change', function() {
console.log('Selected items:', this.selectedItems());
});
</script>
每次更改选择后, 转到演示页面并打开浏览器控制台以查看选定的项目。
浏览器支持
如果我们看一下浏览器的支持 ,就会发现Web组件仅受Chrome和Opera的完全支持。 尽管如此,我们仍然可以将Web组件与polyfills webcomponentjs套件一起使用,从而可以在所有浏览器的最新版本中使用Web组件。
让我们应用此polyfill以便能够在所有浏览器中使用我们的多重选择。 它可以与Bower一起安装,然后包含在您的网页中。
bower install webcomponentsjs
如果在Safari中打开演示页面,则会在控制台中看到错误“ null不是对象” 。 问题是document.currentScript
不存在。 要解决此问题,我们需要从polyfilled环境中获取ownerDocument
(使用document._currentScript
而不是document.currentScript
)。
var ownerDocument = (document._currentScript || document.currentScript).ownerDocument;
有用! 但是,如果您在Safari中打开多选,则会看到列表样式未设置样式。 要解决此其他问题,我们需要填充模板内容的样式。 可以使用WebComponents.ShadowCSS.shimStyling
方法来完成。 我们应该在附加阴影根目录内容之前调用它:
multiselectPrototype.createRootElement = function() {
var root = this.createShadowRoot();
var content = document.importNode(template.content, true);
if (window.ShadowDOMPolyfill) {
WebComponents.ShadowCSS.shimStyling(content, 'x-multiselect');
}
root.appendChild(content);
return root;
};
恭喜你! 现在,我们的多选组件可以正常工作,并且在所有现代浏览器中都可以正常显示。
Web组件polyfill很棒! 为了使这些规范在所有现代浏览器上都能正常工作,显然需要付出巨大的努力。 polyfill源脚本的大小为258Kb。 尽管缩小和压缩的版本为38Kb,但我们可以想象幕后隐藏了多少逻辑。 它不可避免地影响性能。 尽管作者使垫片越来越好地强调了性能。
聚合物和X标签
关于Web组件,我应该提到Polymer 。 Polymer是在Web组件之上构建的库,可简化组件的创建并提供大量现成的元素。 webcomponents.js
是Polymer的一部分,被称为platform.js
。 后来,它被提取并重命名 。
使用Polymer创建Web组件更加容易。 Pankaj Parashar的这篇文章展示了如何使用Polymer来创建Web组件。
如果您想加深话题,这是一些有用的文章列表:
还有另一个库可以简化Web组件的使用,这就是X-Tag 。 它由Mozilla开发,现在得到Microsoft的支持。
结论
Web组件是Web开发领域的一大进步。 它们有助于简化组件的提取,增强封装和使标记更具表现力。
在本教程中,我们已经看到了如何使用Web组件构建可用于生产的多选窗口小部件。 尽管缺少浏览器支持,但由于高质量的polyfill webcomponentsjs,我们今天仍可以使用Web Components。 诸如Polymer和X-Tag之类的库提供了以更简单的方式创建Web组件的机会。
现在,请务必查看后续文章: 如何制作可访问的Web组件 。
您是否已在Web应用程序中使用过Web组件? 请在下面的部分中随意分享您的经验和想法。
From: https://www.sitepoint.com/creating-a-multiselect-component-as-a-web-component/