As developers, we all know the importance of modularization, standardization, and code reuse. The front end has never stopped trying to achieve componentization, resulting in various componentization technologies, from front-end frameworks like Vue and React to full-site packaging tools like webpack.
However, the front end has always lacked a modular standard and a browser-level native componentization solution.
Web Components is the web componentization solution that WHATWG and W3C are trying to implement, providing browser-level support for componentized front-end development. It consists of four main technologies: Shadow DOM, Custom Elements, HTML Import, and HTML Template.
The Polymer project is Google's framework based on the Web Components mechanism, aimed at simple polyfills and ease of use, including data binding, template declaration, event systems, etc. Google has already applied it to YouTube last year.
Polymer 3.0 was just released 20 days ago, and coincidentally, Bilibili's player needs to refactor all UI components recently, so a survey was conducted. All demos below are hosted on polymer-demos. These small demos are just for simple experiences. To understand the complete functionality of Polymer, it is recommended to read the official documentation.
Browser Support#
Currently, the biggest obstacle to using Web Components is the low level of browser support, and the size of Polyfills is relatively large (90+kb).
Currently, only the new versions of Chrome, Opera, and Safari can provide complete native support. For specific support details, you can refer to caniuse.com. After using Polyfills, support can extend to Edge, IE11+, Firefox, and Safari9+.
Polyfills consist of three main files:
webcomponents-bundle.js
: Contains all polyfills.webcomponents-loader.js
: Can detect browser support and then load the corresponding polyfills, reducing unnecessary waste for browsers with native support.custom-elements-es5-adapter.js
: Requires ES6 syntax when registering Custom Elements, so additional processing is needed when the browser does not support ES6; just reference this file.
In general, the best practice for compatibility with the most browsers is as follows:
<scirpt src="webcompoments-loader.js"></scirpt>
<scirpt src="custom-elements-es5-adapter.js"></scirpt>
<script src="index.js"></script>
Among them, webcompoments-loader.js
must be referenced separately, while custom-elements-es5-adapter.js
can be combined with polymer
and your code using Webpack, but be careful not to do additional compilation on custom-elements-es5-adapter.js
. Other code should be compiled to ES5 using Babel. For complete practice, you can refer to polymer-demos.
Custom Elements#
Let's try to define the simplest custom element by inheriting a class from PolymerElement
and then passing it to window.customElements.define
.
Effect
{% raw %}
{% endraw %}
HTML Code
<demo-custom-elements></demo-custom-elements>
JS Code
import { PolymerElement } from '@polymer/polymer';
class DemoCustomElements extends PolymerElement {
constructor() {
super();
this.textContent = `I'm a custom element.`;
}
}
window.customElements.define('demo-custom-elements', DemoCustomElements);
Shadow DOM#
Shadow DOM is a hidden, independent DOM, whose HTML, CSS, and behavior are separated from the regular DOM tree, so that different functionalities do not mix, and the CSS inside and outside do not affect each other.
Shadow DOM is not a new thing; browsers have used it to encapsulate the internal structure of an element. For example, with the <video>
element, what you can see is just a <video>
tag, but in its Shadow DOM, there is a series of buttons and controllers.
In the following example, the p
tag in the Shadow DOM defines the CSS property color
, which will not leak to the outside.
Effect
{% raw %}
I am outside of demo-shadow-dom. Because of encapsulation, demo-shadow-dom's styles won't leak to me.
{% endraw %}HTML Code
<style>
html {
--my-background: #eee;
}
</style>
<demo-shadow-dom></demo-shadow-dom>
<p>I am outside of demo-shadow-dom. Because of encapsulation, demo-shadow-dom's styles won't leak to me.</p>
JS Code
import { PolymerElement, html } from '@polymer/polymer';
export class DemoShadowDom extends PolymerElement {
static get template () {
return html`
<style>
p {
color: #F5712C;
background-color: var(--my-background);
}
</style>
<p>I'm a DOM element.</p>
<p>This is my shadow DOM!</p>
`;
}
}
window.customElements.define('demo-shadow-dom', DemoShadowDom);
HTML Templates#
Using <template>
and <slot>
to compose the shadow DOM.
Effect
{% raw %}
I'm a custom slot.
{% endraw %}
HTML Code
<demo-html-template>
<p>I'm a custom slot.</p>
</demo-html-template>
JS Code
import { PolymerElement, html } from '@polymer/polymer';
import '@polymer/polymer/lib/elements/dom-repeat.js'
import { DemoShadowDom } from './demo-shadow-dom';
class DemoHTMLTemplate extends DemoShadowDom {
constructor() {
super();
this.employees = [
{
name: 'Blog',
link: 'https://diygod.me'
},
{
name: 'GitHub',
link: 'https://github.com/DIYgod'
},
];
}
static get template () {
return html`
<strong>Template:</strong>
<template is="dom-repeat" items="{{employees}}">
<p><a href="{{item.link}}">{{item.name}}</a></p>
</template>
<strong>Slot:</strong>
<slot></slot>
<strong>Super template:</strong>
${super.template}
`;
}
}
window.customElements.define('demo-html-template', DemoHTMLTemplate);
Data Binding#
Supports two-way data binding. You can try editing the input box below, or directly modify the property document.querySelector('demo-data').owner1 = 'DIYgay'
in the console. The property change will be immediately reflected in the DOM.
Effect
{% raw %}
{% endraw %}
HTML Code
<demo-data owner1="DIYgod1"></demo-data>
JS Code
import { PolymerElement, html } from '@polymer/polymer';
import '@polymer/iron-input';
class DemoData extends PolymerElement {
constructor() {
super();
this.owner3 = 'DIYgod3';
}
static get properties () {
return {
owner1: {
type: String,
value: 'DIYgod',
},
owner2: {
type: String,
value: 'DIYgod2',
}
};
}
static get template () {
return html`
<p>This is <b>[[owner1]]</b>'s element.</p>
<p>This is <b>[[owner2]]</b>'s element.</p>
<p>This is <b>{{owner3}}</b>'s element.</p>
<iron-input bind-value="{{owner1}}">
<input is="iron-input" placeholder="Your name here...">
</iron-input>
`;
}
}
window.customElements.define('demo-data', DemoData);
Custom Events#
Next, we will define a custom event named diygod
for our custom element. The method to bind the event callback is the same as for normal events.
Effect
{% raw %}
{% endraw %}
HTML Code
<demo-events></demo-events>
<script>
document.querySelector('demo-events').addEventListener('diygod', function (e) {
alert(e.detail.msg);
});
</script>
JS Code
import { PolymerElement, html } from '@polymer/polymer';
export class DemoEvents extends PolymerElement {
static get template () {
return html`
<button on-click="handleClick">Kick Me</button>
`;
}
handleClick(e) {
this.dispatchEvent(new CustomEvent('diygod', {
detail: {
msg: 'diygod event fired'
}
}));
}
}
window.customElements.define('demo-events', DemoEvents);