# 如何写好 JavaScript (组件封装)
组件是指 web 页面上抽出来的一个个包含模板 (HTML)、功能(JS)和样式(CSS)的单元,好的组件具备封装性、正确性、扩展性和复用性。虽然现在由于有很多优秀的组件存在,往往我们不需要去自己设计一个组件,但我们也要去试着了解他们的实现。
举个栗子:用原生 JS 写一个电商网站的轮播图,应该怎么实现?
结构:HTML 中的无序列表(
<ul>
)- 轮播图是典型的列表结构,可以用无序列表
<ul>
元素来实现,每个图放在一个 li 标签中。
<div id="my-slider" class="slider-list">
<ul>
<li class="slider-list__item--selected">
<img src="https://p5.ssl.qhimg.com/t0119c74624763dd070.png"/>
</li>
<li class="slider-list__item">
<img src="https://p4.ssl.qhimg.com/t01adbe3351db853eb3.jpg"/>
</li>
<li class="slider-list__item">
<img src="https://p2.ssl.qhimg.com/t01645cd5ba0c3b60cb.jpg"/>
</li>
<li class="slider-list__item">
<img src="https://p4.ssl.qhimg.com/t01331ac159b58f5478.jpg"/>
</li>
</ul>
</div>
- 轮播图是典型的列表结构,可以用无序列表
表现:CSS 绝对定位
- 使用 CSS 的绝对定位,将图片重叠在一个位置
- 切换状态使用修饰符(modifier)
- selected
- 轮播图切换动画使用 CSS
transition
实现
#my-slider{
position: relative;
width: 790px;
}
.slider-list ul{
list-style-type:none;
position: relative;
padding: 0;
margin: 0;
}
.slider-list__item,
.slider-list__item--selected{
/* 这里使用绝对定位,可以将多张图片重叠在一起,当然要记得给父盒子开相对定位 */
position: absolute;
transition: opacity 1s;
opacity: 0;
text-align: center;
}
.slider-list__item--selected{
transition: opacity 1s;
opacity: 1;
}
行为:JS,可以线创建一个类,封装其中左滑右滑等具体操作,如下所示。
// 创建一个 Slider 类,封装一些 API
class Slider{
constructor(id){
this.container = document.getElementById(id);
this.items = this.container
.querySelectorAll('.slider-list__item, .slider-list__item--selected');
}
// 获取选中的图片元素:通过选择器 `.slider__item--selected` 获得被选中的元素
getSelectedItem(){
const selected = this.container
.querySelector('.slider-list__item--selected');
return selected
}
// 获取选中图片的索引值:返回选中的元素在 items 数组中的位置。
getSelectedItemIndex(){
return Array.from(this.items).indexOf(this.getSelectedItem());
}
// 跳转到指定索引的图片
slideTo(idx){
const selected = this.getSelectedItem();
if(selected){
// 将之前选择的图片标记为普通状态
selected.className = 'slider-list__item';
}
const item = this.items[idx];
if(item){
// 将当前选中的图片标记为选中状态
item.className = 'slider-list__item--selected';
}
}
// 跳转到下一索引的图片:将下一张图片标记为选中状态
slideNext(){
const currentIdx = this.getSelectedItemIndex();
const nextIdx = (currentIdx + 1) % this.items.length;
this.slideTo(nextIdx);
}
// 跳转到上一索引的图片:将上一张图片标记为选中状态
slidePrevious(){
const currentIdx = this.getSelectedItemIndex();
const previousIdx = (this.items.length + currentIdx - 1) % this.items.length;
this.slideTo(previousIdx);
}
}
当我们需要其播放的时候,只需要创建一个实例,通过定时器来操作具体的行为。
const slider = new Slider('my-slider');
setInterval(() => {
slider.slideNext();
}, 1000);
接下来就是在 API 的代码基础上 加入控制流,让轮播图可以自动轮播,也可以手动控制,实现交互效果
使用自定义事件来解耦
class Slider{
constructor(id, cycle = 3000){
this.container = document.getElementById(id);
this.items = this.container.querySelectorAll('.slider-list__item, .slider-list__item--selected');
this.cycle = cycle;
const controller = this.container.querySelector('.slide-list__control');
if(controller){
const buttons = controller.querySelectorAll('.slide-list__control-buttons, .slide-list__control-buttons--selected');
// 鼠标经过某个小圆点,就将此圆点对应的图片显示出来,并且停止循环轮播
controller.addEventListener('mouseover', evt=>{
const idx = Array.from(buttons).indexOf(evt.target);
if(idx >= 0){
this.slideTo(idx);
this.stop();
}
});
// 鼠标移开小圆点,就继续开始循环轮播
controller.addEventListener('mouseout', evt=>{
this.start();
});
// 注册 slide 事件,将选中的图片和小圆点设置为 selected 状态
this.container.addEventListener('slide', evt => {
const idx = evt.detail.index
const selected = controller.querySelector('.slide-list__control-buttons--selected');
if(selected) selected.className = 'slide-list__control-buttons';
buttons[idx].className = 'slide-list__control-buttons--selected';
})
}
// 点击左边小箭头,翻到前一页
const previous = this.container.querySelector('.slide-list__previous');
if(previous){
previous.addEventListener('click', evt => {
this.stop();
this.slidePrevious();
this.start();
evt.preventDefault();
});
}
// 点击右边小箭头,翻到后一页
const next = this.container.querySelector('.slide-list__next');
if(next){
next.addEventListener('click', evt => {
this.stop();
this.slideNext();
this.start();
evt.preventDefault();
});
}
}
getSelectedItem(){
let selected = this.container.querySelector('.slider-list__item--selected');
return selected
}
getSelectedItemIndex(){
return Array.from(this.items).indexOf(this.getSelectedItem());
}
slideTo(idx){
let selected = this.getSelectedItem();
if(selected){
selected.className = 'slider-list__item';
}
let item = this.items[idx];
if(item){
item.className = 'slider-list__item--selected';
}
const detail = {index: idx}
const event = new CustomEvent('slide', {bubbles:true, detail})
this.container.dispatchEvent(event)
}
slideNext(){
let currentIdx = this.getSelectedItemIndex();
let nextIdx = (currentIdx + 1) % this.items.length;
this.slideTo(nextIdx);
}
slidePrevious(){
let currentIdx = this.getSelectedItemIndex();
let previousIdx = (this.items.length + currentIdx - 1) % this.items.length;
this.slideTo(previousIdx);
}
// 定义一个定时器,循环播放
start(){
this.stop();
this._timer = setInterval(()=>this.slideNext(), this.cycle);
}
// 停止循环播放(用户在自己操作的时候要停止自动循环)
stop(){
clearInterval(this._timer);
}
}
const slider = new Slider('my-slider');
slider.start();
总结:组件封装需要注意其结构设计、展现效果、行为设计(API、Event 等)是否达标。
- 结构设计 HTML
- 展现效果 CSS
- 行为设计 JavaScript
- API (功能)
- Event (控制流)
思考:如何来改进这个轮播图?
# 重构 1:插件化,解耦
上面解决方案的类中的构造器实在是太臃肿了,做了很多本来不应该它要做的事,所以我们考虑插件化,将构造器进行简化
先来看看之前的构造函数做了哪些事
constructor(id, cycle = 3000){ | |
this.container = document.getElementById(id); | |
this.items = this.container.querySelectorAll('.slider-list__item, .slider-list__item--selected'); | |
this.cycle = cycle; | |
// 对小圆点的操作控制流 | |
const controller = this.container.querySelector('.slide-list__control'); | |
if(controller){ | |
// 鼠标经过某个小圆点,就将此圆点对应的图片显示出来,并且停止循环轮播 | |
controller.addEventListener('mouseover', evt=>{ | |
// ... | |
}); | |
// 鼠标移开小圆点,就继续开始循环轮播 | |
controller.addEventListener('mouseout', evt=>{ | |
this.start(); | |
}); | |
// 注册 slide 事件,将选中的图片和小圆点设置为 selected 状态 | |
this.container.addEventListener('slide', evt => { | |
// ... | |
} | |
// 点击左边小箭头,翻到前一页 | |
const previous = this.container.querySelector('.slide-list__previous'); | |
// ... | |
} | |
// 点击右边小箭头,翻到后一页 | |
const next = this.container.querySelector('.slide-list__next'); | |
// ... | |
} |
将控制元素抽取成一个个插件(左右小箭头、底下的四个小圆点)等等
解耦: 将控制元素抽取成插件; 插件与组件之间通过依赖注⼊方式建立联系
我们要将用户控制的操作从组件中抽离出来,做成插件,这样就提高了组件的可扩展性!!!
用户的控制组件分为三个部分可以抽离成三个插件。
首先将小圆点的控制抽离成一个插件
pluginController
插件接收的参数就是组件的实例,将控制流中的事件写在这里,插件中的逻辑就是之前构造函数中的逻辑
function pluginController(slider){
// 对小圆点的操作控制流
const controller = slider.container.querySelector('.slide-list__control');
if(controller){
const buttons = controller.querySelectorAll('.slide-list__control-buttons, .slide-list__control-buttons--selected');
// 鼠标经过某个小圆点,就将此圆点对应的图片显示出来,并且停止循环轮播
controller.addEventListener('mouseover', evt=>{
const idx = Array.from(buttons).indexOf(evt.target);
if(idx >= 0){
slider.slideTo(idx);
slider.stop();
}
});
// 鼠标移开小圆点,就继续开始循环轮播
controller.addEventListener('mouseout', evt=>{
slider.start();
});
// 注册 slide 事件,将选中的图片和小圆点设置为 selected 状态
slider.addEventListener('slide', evt => {
const idx = evt.detail.index
const selected = controller.querySelector('.slide-list__control-buttons--selected');
if(selected) selected.className = 'slide-list__control-buttons';
buttons[idx].className = 'slide-list__control-buttons--selected';
});
}
}
将左翻页的控制抽离成插件 pluginPrevious
function pluginPrevious(slider){ | |
const previous = slider.container.querySelector('.slide-list__previous'); | |
if(previous){ | |
previous.addEventListener('click', evt => { | |
slider.stop(); | |
slider.slidePrevious(); | |
slider.start(); | |
evt.preventDefault(); | |
}); | |
} | |
} |
将右翻页的控制抽离成插件 pluginNext
function pluginNext(slider){ | |
const next = slider.container.querySelector('.slide-list__next'); | |
if(next){ | |
next.addEventListener('click', evt => { | |
slider.stop(); | |
slider.slideNext(); | |
slider.start(); | |
evt.preventDefault(); | |
}); | |
} | |
} |
最后我们的组件就是这样定义的
class Slider{ | |
constructor(id, cycle = 3000) { | |
this.container = document.getElementById(id); | |
this.items = this.container.querySelectorAll( | |
".slider-list__item, .slider-list__item--selected" | |
); | |
this.cycle = cycle; | |
} | |
registerPlugins(...plugins){ | |
// 这里的 this 就是组件的实例对象 | |
plugins.forEach(plugin => plugin(this)); | |
} | |
getSelectedItem(){ | |
const selected = this.container.querySelector('.slider-list__item--selected'); | |
return selected | |
} | |
getSelectedItemIndex(){ | |
return Array.from(this.items).indexOf(this.getSelectedItem()); | |
} | |
slideTo(idx){ | |
const selected = this.getSelectedItem(); | |
if(selected){ | |
selected.className = 'slider-list__item'; | |
} | |
const item = this.items[idx]; | |
if(item){ | |
item.className = 'slider-list__item--selected'; | |
} | |
const detail = {index: idx} | |
const event = new CustomEvent('slide', {bubbles:true, detail}) | |
this.container.dispatchEvent(event) | |
} | |
slideNext(){ | |
const currentIdx = this.getSelectedItemIndex(); | |
const nextIdx = (currentIdx + 1) % this.items.length; | |
this.slideTo(nextIdx); | |
} | |
slidePrevious(){ | |
const currentIdx = this.getSelectedItemIndex(); | |
const previousIdx = (this.items.length + currentIdx - 1) % this.items.length; | |
this.slideTo(previousIdx); | |
} | |
addEventListener(type, handler){ | |
this.container.addEventListener(type, handler) | |
} | |
start(){ | |
this.stop(); | |
this._timer = setInterval(()=>this.slideNext(), this.cycle); | |
} | |
stop(){ | |
clearInterval(this._timer); | |
} | |
} | |
// 下面是插件的定义 | |
function pluginController(slider) { | |
// 对小圆点的操作控制流 | |
const controller = slider.container.querySelector(".slide-list__control"); | |
if (controller) { | |
const buttons = controller.querySelectorAll( | |
".slide-list__control-buttons, .slide-list__control-buttons--selected" | |
); | |
// 鼠标经过某个小圆点,就将此圆点对应的图片显示出来,并且停止循环轮播 | |
controller.addEventListener("mouseover", (evt) => { | |
const idx = Array.from(buttons).indexOf(evt.target); | |
if (idx >= 0) { | |
slider.slideTo(idx); | |
slider.stop(); | |
} | |
}); | |
// 鼠标移开小圆点,就继续开始循环轮播 | |
controller.addEventListener("mouseout", (evt) => { | |
slider.start(); | |
}); | |
// 注册 slide 事件,将选中的图片和小圆点设置为 selected 状态 | |
slider.addEventListener("slide", (evt) => { | |
const idx = evt.detail.index; | |
const selected = controller.querySelector( | |
".slide-list__control-buttons--selected" | |
); | |
if (selected) selected.className = "slide-list__control-buttons"; | |
buttons[idx].className = "slide-list__control-buttons--selected"; | |
}); | |
} | |
} | |
function pluginPrevious(slider) { | |
const previous = slider.container.querySelector(".slide-list__previous"); | |
if (previous) { | |
previous.addEventListener("click", (evt) => { | |
slider.stop(); | |
slider.slidePrevious(); | |
slider.start(); | |
evt.preventDefault(); | |
}); | |
} | |
} | |
function pluginNext(slider) { | |
const next = slider.container.querySelector(".slide-list__next"); | |
if (next) { | |
next.addEventListener("click", (evt) => { | |
slider.stop(); | |
slider.slideNext(); | |
slider.start(); | |
evt.preventDefault(); | |
}); | |
} | |
} |
最后使用的时候也很简单
const slider = new Slider('my-slider');// 内部通过 id 查找 | |
// 注册三个插件 | |
slider.registerPlugins(pluginController, pluginPrevious, pluginNext); | |
slider.start(); |
此时的构造函数已经精简了,我们将 JS 进行了解耦,通过注册插件 registerPlugins
来使用各种插件(控件)~
这种将依赖对象传入插件初始化函数的方式,叫做依赖注入,这是一种组件与插件解耦的基本思路
进行插件化之后,我们可以任意组合我们想要的插件,比如我们将底部小圆点插件去除
slider.registerPlugins(pluginPrevious, pluginNext);
可以看到下方的小圆点已经不生效了 (注意看上面动图小圆点已经不动了),但是这里有了新的问题,下方小圆点虽然失效了,但是没有消失,我们要是将小圆点也去除就要手动去操作 HTML 了~
所以我们要继续对组件进行重构!我们解耦 HTML,让 JavaScript 来渲染组件的结构 —— 模板化
# 重构 2:模板化
在组件中加入了 render()
渲染函数,用来渲染 HTML
class Slider { | |
constructor(id, opts = { | |
images: [], | |
cycle: 3000 | |
}) { | |
this.container = document.getElementById(id); | |
this.options = opts; | |
this.container.innerHTML = this.render(); | |
this.items = this.container.querySelectorAll('.slider-list__item, .slider-list__item--selected'); | |
this.cycle = opts.cycle || 3000; | |
this.slideTo(0); | |
} | |
render() { | |
const images = this.options.images; | |
const content = images.map(image => ` | |
<li class="slider-list__item"> | |
<img src="${image}"/> | |
</li> | |
`.trim()); | |
return `<ul>${content.join('')}</ul>`; | |
} | |
} |
这里将图片放入一个 images 数组中,这样就可以让组件拓展成 指定任意多的图片的 轮播图
接下来定义下三个插件(插件从一个函数变成一个对象,对象中有两个函数,一个渲染 HTML,一个注册自定义事件 JS)
下部小圆点的插件是这样定义的,插件中也要定义 render()
渲染函数, action()
用来注册自定义事件
const pluginController = { | |
render(images) { | |
return ` | |
<div class="slide-list__control"> | |
${images.map((image, i) => ` | |
<span class="slide-list__control-buttons${i===0?'--selected':''}"></span> | |
`).join('')} | |
</div> | |
`.trim(); | |
}, | |
action(slider) { | |
const controller = slider.container.querySelector('.slide-list__control'); | |
if (controller) { | |
const buttons = controller.querySelectorAll( | |
'.slide-list__control-buttons, .slide-list__control-buttons--selected'); | |
controller.addEventListener('mouseover', evt => { | |
const idx = Array.from(buttons).indexOf(evt.target); | |
if (idx >= 0) { | |
slider.slideTo(idx); | |
slider.stop(); | |
} | |
}); | |
controller.addEventListener('mouseout', evt => { | |
slider.start(); | |
}); | |
slider.addEventListener('slide', evt => { | |
const idx = evt.detail.index | |
const selected = controller.querySelector('.slide-list__control-buttons--selected'); | |
if (selected) selected.className = 'slide-list__control-buttons'; | |
buttons[idx].className = 'slide-list__control-buttons--selected'; | |
}); | |
} | |
} | |
}; |
向前翻页的插件是这样定义的
const pluginPrevious = { | |
render() { | |
return `<a class="slide-list__previous"></a>`; | |
}, | |
action(slider) { | |
const previous = slider.container.querySelector('.slide-list__previous'); | |
if (previous) { | |
previous.addEventListener('click', evt => { | |
slider.stop(); | |
slider.slidePrevious(); | |
slider.start(); | |
evt.preventDefault(); | |
}); | |
} | |
} | |
}; |
向后翻页同理
const pluginNext = { | |
render() { | |
return `<a class="slide-list__next"></a>`; | |
}, | |
action(slider) { | |
const previous = slider.container.querySelector('.slide-list__next'); | |
if (previous) { | |
previous.addEventListener('click', evt => { | |
slider.stop(); | |
slider.slideNext(); | |
slider.start(); | |
evt.preventDefault(); | |
}); | |
} | |
} | |
}; |
注册插件是这样定义的,渲染 HTML 结构,绑定 JS 事件行为
registerPlugins(...plugins) { | |
plugins.forEach(plugin => { | |
const pluginContainer = document.createElement('div'); | |
pluginContainer.className = '.slider-list__plugin'; | |
pluginContainer.innerHTML = plugin.render(this.options.images); | |
this.container.appendChild(pluginContainer); | |
plugin.action(this); | |
}); | |
} |
将 HTML 解耦后,我们的 HTML 就只需要一个盒子就可以了
<div id="my-slider" class="slider-list"></div> | |
复制代码 |
最后,我们是这样来使用这个组件的
const slider = new Slider('my-slider', { | |
images: ['https://p5.ssl.qhimg.com/t0119c74624763dd070.png', | |
'https://p4.ssl.qhimg.com/t01adbe3351db853eb3.jpg', | |
'https://p2.ssl.qhimg.com/t01645cd5ba0c3b60cb.jpg', | |
'https://p4.ssl.qhimg.com/t01331ac159b58f5478.jpg' | |
], | |
cycle: 1000 | |
}); | |
slider.registerPlugins(pluginController, pluginPrevious, pluginNext); | |
slider.start(); |
完美其实还可以解耦 CSS,以后有时间再探索探索吧~
# 重构 3:抽象化
将通用的组件模型,抽象出来一个组件类(Component),其他组件类通过继承该类并实现其 render 方法。
class Component{ | |
constructor(id, opts = {name, data: []}) { | |
this.container = document.getElementById(id); | |
this.options = opts; | |
this.container.innerHTML = this.render(opts.data); | |
} | |
registerPlugins(...plugins) { | |
plugins.forEach( plugin => { | |
const pluginContainer = document.createElement( 'div'); | |
pluginContainer.className = `${name}__plugin`; | |
pluginContainer.innerHTML = plugin.render(this.options.data); | |
this.container.appendchild(pluginContainer); | |
plugin.action(this); | |
}); | |
} | |
render(data) { | |
/* abstract */ | |
return '' | |
} | |
} |
让轮播图组件 继承自 我们定义的通用组件
class Slider extends Component { | |
constructor(id, opts = { | |
name: 'slider-list', | |
data: [], | |
cycle: 3000 | |
}) { | |
super(id, opts); | |
this.items = this.container.querySelectorAll('.slider-list__item, .slider-list__item--selected'); | |
this.cycle = opts.cycle || 3000; | |
this.slideTo(0); | |
} | |
render(data) { | |
const content = data.map(image => ` | |
<li class="slider-list__item"> | |
<img src="${image}"/> | |
</li> | |
`.trim()); | |
return `<ul>${content.join('')}</ul>`; | |
} | |
getSelectedItem() { | |
const selected = this.container.querySelector('.slider-list__item--selected'); | |
return selected | |
} | |
getSelectedItemIndex() { | |
return Array.from(this.items).indexOf(this.getSelectedItem()); | |
} | |
slideTo(idx) { | |
const selected = this.getSelectedItem(); | |
if (selected) { | |
selected.className = 'slider-list__item'; | |
} | |
const item = this.items[idx]; | |
if (item) { | |
item.className = 'slider-list__item--selected'; | |
} | |
const detail = { | |
index: idx | |
} | |
const event = new CustomEvent('slide', { | |
bubbles: true, | |
detail | |
}) | |
this.container.dispatchEvent(event) | |
} | |
slideNext() { | |
const currentIdx = this.getSelectedItemIndex(); | |
const nextIdx = (currentIdx + 1) % this.items.length; | |
this.slideTo(nextIdx); | |
} | |
slidePrevious() { | |
const currentIdx = this.getSelectedItemIndex(); | |
const previousIdx = (this.items.length + currentIdx - 1) % this.items.length; | |
this.slideTo(previousIdx); | |
} | |
addEventListener(type, handler) { | |
this.container.addEventListener(type, handler); | |
} | |
start() { | |
this.stop(); | |
this._timer = setInterval(() => this.slideNext(), this.cycle); | |
} | |
stop() { | |
clearInterval(this._timer); | |
} | |
} |
三个插件
小圆点插件
const pluginController = { | |
render(images) { | |
return ` | |
<div class="slide-list__control"> | |
${images.map((image, i) => ` | |
<span class="slide-list__control-buttons${i===0?'--selected':''}"></span> | |
`).join('')} | |
</div> | |
`.trim(); | |
}, | |
action(slider) { | |
let controller = slider.container.querySelector('.slide-list__control'); | |
if (controller) { | |
let buttons = controller.querySelectorAll( | |
'.slide-list__control-buttons, .slide-list__control-buttons--selected'); | |
controller.addEventListener('mouseover', evt => { | |
var idx = Array.from(buttons).indexOf(evt.target); | |
if (idx >= 0) { | |
slider.slideTo(idx); | |
slider.stop(); | |
} | |
}); | |
controller.addEventListener('mouseout', evt => { | |
slider.start(); | |
}); | |
slider.addEventListener('slide', evt => { | |
const idx = evt.detail.index; | |
let selected = controller.querySelector('.slide-list__control-buttons--selected'); | |
if (selected) selected.className = 'slide-list__control-buttons'; | |
buttons[idx].className = 'slide-list__control-buttons--selected'; | |
}); | |
} | |
} | |
}; |
向前翻页插件
const pluginPrevious = { | |
render() { | |
return `<a class="slide-list__previous"></a>`; | |
}, | |
action(slider) { | |
let previous = slider.container.querySelector('.slide-list__previous'); | |
if (previous) { | |
previous.addEventListener('click', evt => { | |
slider.stop(); | |
slider.slidePrevious(); | |
slider.start(); | |
evt.preventDefault(); | |
}); | |
} | |
} | |
}; |
向后翻页插件
const pluginNext = { | |
render() { | |
return `<a class="slide-list__next"></a>`; | |
}, | |
action(slider) { | |
let previous = slider.container.querySelector('.slide-list__next'); | |
if (previous) { | |
previous.addEventListener('click', evt => { | |
slider.stop(); | |
slider.slideNext(); | |
slider.start(); | |
evt.preventDefault(); | |
}); | |
} | |
} | |
}; |
使用我们的组件,使用方式不变
const slider = new Slider('my-slider', { | |
name: 'slide-list', | |
data: ['https://p5.ssl.qhimg.com/t0119c74624763dd070.png', | |
'https://p4.ssl.qhimg.com/t01adbe3351db853eb3.jpg', | |
'https://p2.ssl.qhimg.com/t01645cd5ba0c3b60cb.jpg', | |
'https://p4.ssl.qhimg.com/t01331ac159b58f5478.jpg' | |
], | |
cycle: 1000 | |
}); | |
slider.registerPlugins(pluginController, pluginPrevious, pluginNext); | |
slider.start(); |
总结:
- 组件设计的原则 —— 封装性、正确性、拓展性和复用性
- 实现步骤:结构设计、展现效果、行为设计
- 三次重构
- 插件化
- 模板化
- 抽象化
- 改进:CSS 模板化、父子组件的状态同步和消息通信等等
虽然现在有很多组件库比如 Vue 还有 React 中的组件模式,但是我们自己研究一下这里面的机制与原理对我们理解组件库以及 JavaScript 还是会很有帮助的!!
这样的不断解耦 JS 实现插件化,解耦 HTML 实现模板化,甚至还可以解耦 CSS,这中思路提供了代码设计和抽象的一套通用规范,而遵循这套规范的基础库,实际上就是完整的 UI 组件框架!!!。
# 最后附上这次的所有源码
初级
解耦
模板化
终极