学习 Web Component 的原因
这是学习 Vue 插槽前的理论铺垫:
- 通过 Web Component 的学习, 我们能更好地理解 Vue 插槽设计的初衷和原有的规范准则
- Vue 的组件模板系统是参考了 Web Component 的规范来进行上层设计的!
什么是 Web Component?
在了解 Web Component 之前,我们先来聊一下在没有 Web Component 之前页面开发的一些痛点:
- 在 ES3,ES5 的年代,JS 内部定义模板困难, 举个例子:
js
var title = '我是标题';
var content = '我是内容';
var view = '<div>'
+ '\n'
+ ' <h1>'
+ title
+ ' </h1>'
+ '\n'
+ ' <p>'
+ '\n'
+ content
+ ' </p>'
+ '\n'
+ '</div>'
- 在 ES6 出来了之后,可以使用模板字符串定义视图。虽说方便了不少,但是还是标准规范不统一。(毕竟 JS 这门编程语言实在是太灵活了 😂)
js
const title = '我是标题';
const content = '我是内容';
// 我想直接使用面向过程来写
var view = `
<div>
<h1>${title}</h1>
<p>${content}</p>
</div>
`;
// 我想使用函数式编程来写
function createView(title, content) {
return `
<div>
<h1>${title}</h1>
<p>${content}</p>
</div>
`;
}
createView(title, content);
// 我想用面相对象的方式来写
class ViewCreator {
constructor(title, content) {
this.title = title;
this.content = content;
}
createView() {
const { title, content } = this;
return `
<div>
<h1>${title}</h1>
<p>${content}</p>
</div>
`;
}
}
const viewCreator = new ViewCreator(title, content);
viewCreator.createView();
而 Web Component 的出现,就是为了统一自定义组件标准的。
Web Components 的优势:
- 自定义标签:Web Component API 允许开发者自定义组件标签,然后直接在 HTML 模板中使用
- 自定义传导属性:Web Component 可以将标签上面的属性吸收进来,并提供给真实需要渲染组件视图的元素使用
- 自定义插槽:【插槽】这个概念最先就是由 Web Component 提出的,它相当于给定了一个视图的预设,并在模板渲染的时候通过
slot="{slotName}"
将定义位置的插槽替换掉。 - HTML/DOM 原则上的支持:
- 在原生的 JavaScript 中, HTML / DOM 本身就规范了 Web components (视图的复用规范)
- HTML / DOM 希望有方案提供给开发者可自定义可重用, 可被浏览器正常解析的标签; 让逻辑样式封装在一个组件中, 最终用自定义标签渲染视图
关于 Web Component 的一些术语
不需要掌握,先混个眼熟 😁
1. window.customElements
window.customElements
是挂载 window 上面一个对象,它里面有一个 define()
方法又来自定义元素
2. shadowDOM
shadowDOM (可以理解为影子 DOM), 一个游离在真实 DOM 之外的一个影子 DOM 对象。
- shadowDOM 不会直接渲染到真实的文档中
- Web Component 一旦匹配到了 shadowDOM 中的 shadowRoot 中的内容,就会进行渲染
- 在 HTMLElement 的子类中使用
attchWindow()
方法获取真实的 DOM 元素
Web Components渲染的两种方式
1. 根据自定义模板创建
使用步骤:
- 创建 Web Component
js
class MyInfo extends HTMLElement {
constructor() {
super();
this.title = this.textContent;
this.avatar = this.getAttribute('avatar');
this.myName = this.getAttribute('name');
this.age = this.getAttribute('age');
this.occupation = this.getAttribute('occupation');
this.init();
}
init() {
const shadowDOM = this.attachShadow({ mode: 'open' });
shadowDOM.appendChild(this.createDOM());
}
createDOM() {
var oContainer = this.createContainer();
oContainer.appendChild(this.createTitle());
oContainer.appendChild(this.createAvatar());
oContainer.appendChild(this.createName());
oContainer.appendChild(this.createAge());
oContainer.appendChild(this.createOccupation());
return oContainer;
}
createContainer() {
var oContainer = document.createElement('div');
oContainer.className = 'my-info-container';
return oContainer;
}
createTitle() {
var oTitle = document.createElement('h1');
oTitle.className = 'my-info-title';
oTitle.textContent = this.title;
return oTitle;
}
createAvatar() {
var oAvatar = document.createElement('div');
oAvatar.className = 'my-info-avatar';
oAvatar.innerHTML = `<img style="width: 100px;" src="${this.avatar}" />`
return oAvatar;
}
createName() {
var oName = document.createElement('p');
oName.className = 'my-info-name';
oName.textContent = `Name: ${this.myName}`;
return oName;
}
createAge() {
var oAge = document.createElement('p');
oAge.className = 'my-info-age';
oAge.textContent = `Age: ${this.age}`;
return oAge;
}
createOccupation() {
var oOccupation = document.createElement('p');
oOccupation.className = 'my-info-occupation';
oOccupation.textContent = `Occupation: ${this.occupation}`;
return oOccupation;
}
}
window.customElements.define('my-info', MyInfo);
- 使用 Web Component 组件
html
<my-info
avatar="https://p6-passport.byteacctimg.com/img/user-avatar/714b47c68e75465a5c5108aa8a7887b2~80x80.awebp"
name="Zhangsan"
age="25"
occupation="programmer"
>
lalala
</my-info>
完整代码:
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<my-info
avatar="https://p6-passport.byteacctimg.com/img/user-avatar/714b47c68e75465a5c5108aa8a7887b2~80x80.awebp"
name="Zhangsan"
age="25"
occupation="programmer"
>
lalala
</my-info>
<script type="text/javascript">
class MyInfo extends HTMLElement {
constructor() {
super();
this.title = this.textContent;
this.avatar = this.getAttribute('avatar');
this.myName = this.getAttribute('name');
this.age = this.getAttribute('age');
this.occupation = this.getAttribute('occupation');
this.init();
}
init() {
const shadowDOM = this.attachShadow({ mode: 'open' });
shadowDOM.appendChild(this.createDOM());
}
createDOM() {
var oContainer = this.createContainer();
oContainer.appendChild(this.createTitle());
oContainer.appendChild(this.createAvatar());
oContainer.appendChild(this.createName());
oContainer.appendChild(this.createAge());
oContainer.appendChild(this.createOccupation());
return oContainer;
}
createContainer() {
var oContainer = document.createElement('div');
oContainer.className = 'my-info-container';
return oContainer;
}
createTitle() {
var oTitle = document.createElement('h1');
oTitle.className = 'my-info-title';
oTitle.textContent = this.title;
return oTitle;
}
createAvatar() {
var oAvatar = document.createElement('div');
oAvatar.className = 'my-info-avatar';
oAvatar.innerHTML = `<img style="width: 100px;" src="${this.avatar}" />`
return oAvatar;
}
createName() {
var oName = document.createElement('p');
oName.className = 'my-info-name';
oName.textContent = `Name: ${this.myName}`;
return oName;
}
createAge() {
var oAge = document.createElement('p');
oAge.className = 'my-info-age';
oAge.textContent = `Age: ${this.age}`;
return oAge;
}
createOccupation() {
var oOccupation = document.createElement('p');
oOccupation.className = 'my-info-occupation';
oOccupation.textContent = `Occupation: ${this.occupation}`;
return oOccupation;
}
}
window.customElements.define('my-info', MyInfo);
</script>
</body>
</html>
2. 根据已有的模板克隆创建
使用步骤:
- 创建一段可以需要被复用的模板 (这里面定义了一些插槽)
html
<template id="my-article-template">
<style>
h1 {
color: red;
}
h1 .author,
h1 .date-time {
font-size: 16px;
color: #666;
font-weight: normal;
}
</style>
<div class="my-article">
<h1 class="my-article-title">
<slot name="title" class="title"></slot>
<slot name="author" class="author"></slot>
<slot name="dateTime" class="date-time"></slot>
</h1>
<p class="my-article-content">
<slot name="content"></slot>
</p>
</div>
</template>
- 创建 Web Component 组件
js
class MyArticle extends HTMLElement {
constructor() {
super();
this.init();
}
init() {
const shadowDOM = this.attachShadow({ mode: 'open' });
const template = document.getElementById('my-article-template').content;
shadowDOM.appendChild(template.cloneNode(true));
}
}
window.customElements.define('my-article', MyArticle);
- 使用 Web Component 组件
html
<my-article>
<p slot="title">This is my title</p>
<span slot="author">Author</span>
<span slot="dateTime"> - 14:15</span>
<p slot="content">This is my content</p>
</my-article>
完整代码:
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<template id="my-article-template">
<style>
h1 {
color: red;
}
h1 .author,
h1 .date-time {
font-size: 16px;
color: #666;
font-weight: normal;
}
</style>
<div class="my-article">
<h1 class="my-article-title">
<slot name="title" class="title"></slot>
<slot name="author" class="author"></slot>
<slot name="dateTime" class="date-time"></slot>
</h1>
<p class="my-article-content">
<slot name="content"></slot>
</p>
</div>
</template>
<my-article>
<p slot="title">This is my title</p>
<span slot="author">Author</span>
<span slot="dateTime"> - 14:15</span>
<p slot="content">This is my content</p>
</my-article>
<script type="text/javascript">
class MyArticle extends HTMLElement {
constructor() {
super();
this.init();
}
init() {
const shadowDOM = this.attachShadow({ mode: 'open' });
const template = document.getElementById('my-article-template').content;
shadowDOM.appendChild(template.cloneNode(true));
}
}
window.customElements.define('my-article', MyArticle);
</script>
</body>
</html>