写好JS的原则之组件封装

写好JS的原理

应当遵循以下的三个原则:

各司其职
组件封装
过程抽象

组件封装

什么是组件?组件是指Web页面上抽出来一个个包含模板(HTML)、功能(JS)和样式(CSS)的单元

一个好的组件特点:

  • 封装性、正确性、扩展性、复用性。

例子

使用原生JS写一个网站的轮播图,应该怎么实现?

结构设计:HTML

点击这里可以查看完整代码及效果

结构:HTML
轮播图是一个典型的列表结构,可以使用无序列表<ul>来实现

    <div id="my-slider" class="slider-list">
        <ul>
            <li class="slider-list__item"><img src="./img/1.png"></li>
            <li class="slider-list__item"><img src="./img/2.png"></li>
            <li class="slider-list__item"><img src="./img/3.png"></li>
            <li class="slider-list__item"><img src="./img/4.png"></li>
            <li class="slider-list__item"><img src="./img/5.png"></li>
        </ul>
    </div>

展现效果:CSS

点击这里可以查看完整代码及效果

表现:CSS

  • 使用CSS绝对定位将图片重叠在同一个位置
  • 轮播图切换的状态使用修饰符(modifier)
  • 轮播图的切换动画使用CSS transition
         #my-slider,
        img {
            position: relative;
            width: 790px;
            height: 450px;
        }

        .slider-list ul {
            list-style-type: none;
            position: relative;
            width: 100%;
            height: 100%;
            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;
        }

行为设计:API

点击这里可以查看完整代码及效果

行为:JS
API设计应该保证原子操作,职责单一,瞒住灵活性

  • Slider:API
    • getSelectedItem() 得到当前轮播图正在显示的li
    • getSelectedItemIndex() 获取当前轮播图的显示li的下标
    • sliderTo(idx) 轮播到指定的idx下标的图片
    • sliderNext() 下一张
    • sliderPrevious() 上一张
class Slider {
            constructor(id) {
                this.container = document.getElementById(id);//获取轮播图的容器
                this.items = this.container.querySelectorAll('.slider-list__item, .slider-list__item--selected');//获取当前所有的li
            }
            // 得到当前轮播图显示的li
            getSelectedItem() {
                const selected = this.container.querySelector('.slider-list__item--selected');
                return selected;
            }
            // 获取当前轮播图的显示li的下标
            getSelectedItemIndex() {
                return Array.from(this.items).indexOf(this.getSelectedItem());
            }
            // 轮播到指定的idx下标的图片
            sliderTo(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';
                }
            }
            // 下一张
            sliderNext() {
                const currentIdx = this.getSelectedItemIndex();
                const nextIdx = (currentIdx + 1) % this.items.length;
                this.sliderTo(nextIdx);
            }
            // 上一张
            sliderPrevious() {
                const currentIdx = this.getSelectedItemIndex();
                const previousIdx = (this.items.length + currentIdx - 1) % this.items.length;
                this.sliderTo(previousIdx);
            }
        }
        const slider = new Slider('my-slider');
        
        // API测试
        // slider.sliderTo(3);
        // slider.getSelectedItemIndex()
        // slider.sliderNext();
        // slider.sliderPrevious();

上面的5个API基本覆盖了这个操作轮播的80%的功能,接下来就是去添加下面圆点和控制左右移动按钮,但是这些控制的按钮和组件的状态是有状态耦合的,但是设计中是不需要这些按钮和轮播的图片之间绑定的很死,因为后面可能需要更改需求,比如未来,我们不需要底下圆点,如果设计的很死的话,就修改起来很复杂,因此在设计的时候轮播图片和这些按钮是独立的,因此就在控制流部分需要进行解耦。

行为设计:控制流

点击这里可以查看完整代码及效果

控制流

  • 使用自定义事件来解耦

按钮及上下控制,按钮样式我就不贴了~

       <!--上一页按钮-->
        <a class="slide-list__next"></a>
        <!--下一页按钮-->
        <a class="slide-list__previous"></a>
        <!-- 对应小圆点 -->
        <div class="slide-list__control">
            <span class="slide-list__control-buttons--selected"></span>
            <span class="slide-list__control-buttons"></span>
            <span class="slide-list__control-buttons"></span>
            <span class="slide-list__control-buttons"></span>
            <span class="slide-list__control-buttons"></span>
        </div>

自定义事件

const detail = {index: idx};
const event = new CustomEvent('slide',{
    bubbles:true, 
    detail
    });
this.container.disoatchEvent(event);

为了让图片动起来还需要增加两个start()``stop()API,同时还需要修改sliderTo()API来自定义事件进行解耦,并且自动给圆点容器派发自定义事件,让圆点同步

// 轮播到指定的idx下标的图片
sliderTo(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);
}

// 开始轮播
start() {
    this.stop();//清楚上个定时器
    this._timer = setInterval(() => this.sliderNext(), this.cycle);//开启定时器移动轮播图
}
// 停止轮播
stop() {
    clearInterval(this._timer);
}

最后将按钮的功能添加到constructor里面并且实现自动轮播更新

constructor(id, cycle = 3000) {//id是容器的id, cycle是多少毫秒轮播一次
    this.container = document.getElementById(id);//获取轮播图的容器
    this.items = this.container.querySelectorAll('.slider-list__item, .slider-list__item--selected');//获取当前所有的li
    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.sliderTo(idx);
                this.stop();
            }
        });
        // 给圆点添加鼠标移出的监听事件, 鼠标移出轮播图播放
        controller.addEventListener('mouseout', evt => {
            this.start();
        })
        // 给圆点添加自定义事件,让红色圆点移动和图片同步
        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.sliderPrevious();
            this.start();
            evt.preventDefault();
        })
    }
    // 设置下一页点击事件
    const next = this.container.querySelector('.slide-list__next');
    if (next) {
        next.addEventListener('click', evt => {
            this.stop();
            this.sliderNext();
            this.start();
            evt.preventDefault();
        })
    }
}

这样就完整实现了一个轮播图的所有功能,其效果如下

2.gif

总结:基本方法

  • 结构设计
  • 效果设计
  • 行为设计
    • API(功能)
    • Event(控制流)
      通过上面方法我们完整的构建出来了这个轮播图,但是这个组件只能是一锤子买卖,这样就只具备了好组件的封装性正确性,但是在扩展性复用性就差了一点,也就是说下次想在其他项目中使用这个组件,那你还是需要把代码Copy过去进行修改才能使用,那么还是挺麻烦的,所以这个代码还有改进的空间~

重构:插件化

点击这里可以查看完整代码及效果

在未来如果我们在新的项目里面并不需要这左右按钮或者这五个圆点,那么我们想要的效果是直接在HTML中直接删除或者换掉这部分HTML代码,而不需要去动JS中的核心逻辑代码,因此我们需要把图片轮播和这几个按钮的主体部分分离出来,也就是将左右两个按钮和五个圆点解耦出来
image.png

解耦

  • 将控制元素抽取成插件
  • 插件与组件之间通过依赖注入方式建立联系
    constructor中把控制圆点和上下页移动轮播图的功能抽离出来成为一个个插件函数,然后再在组件里面添加一个注册函数registerPlugins()API,轮播图调用时候,只需要把每个插件函数通过这个函数注册进去就行了,去除就是把插件函数注销掉就行了

注册函数API:

registerPlugins(...plugins) {
    plugins.forEach(plugin => plugin(this))
}

把控制圆点按钮和上下页的插件函数分离成类似与这样的插件

// 圆点按钮插件
function pluginController(slider) {
    ...
}
// 上一页插件
function pluginPrevious(slider) {
    ...
}
// 下一页插件
function pluginNext(slider) {
     ...
}

调用的时候,就只需要像下面这样,去注册需要的插件即可,不需要的则注释掉相应的插件即可,然后从HTML中删除掉相应的结构代码即可,如果需要扩展新的插件,则只需要重新写好插件然后再注册到这个组件里面去就行了,完整的结构就如下:

class Slider {
    constructor(id, cycle = 3000) {//id是容器的id, cycle是多少毫秒轮播一次
        this.container = document.getElementById(id);//获取轮播图的容器
        this.items = this.container.querySelectorAll('.slider-list__item, .slider-list__item--selected');//获取当前所有的li
        this.cycle = cycle;
    }
    registerPlugins(...plugins) {
        plugins.forEach(plugin => plugin(this))
    }

    // 得到当前轮播图显示的li
    getSelectedItem() {
       ...
    }
    // 获取当前轮播图的显示li的下标
    getSelectedItemIndex() {
       ...
    }
    // 轮播到指定的idx下标的图片
    sliderTo(idx) {
        ...
    }

    // 下一张
    sliderNext() {
       ...
    }
    // 上一张
    sliderPrevious() {
        ...
    }
    // 开始轮播
    start() {
        ...
    }
    // 停止轮播
    stop() {
       ...
    }
}
 // 圆点按钮插件
function pluginController(slider) {
    ...
}
// 上一页插件
function pluginPrevious(slider) {
    ...
}
// 下一页插件
function pluginNext(slider) {
     ...
}
const slider = new Slider('my-slider');
slider.registerPlugins(/*pluginController, */pluginPrevious, pluginNext);//注册圆点, 上一页,下一页事件
slider.start();

重构:模板化

点击这里可以查看完整代码及效果

虽然现在把插件的功能独立了出来,如果我们注释掉了圆点的插件, 但是在HTML里的圆点还是存在,只是功能不见了,还需要我们在HTML手动删除,这样的话还是需要我们去改多个地方,所以如果还需要更好的扩展性的话,我们应该把HTML模板化,这样更易于扩展

image.png

HTML只需要一个容器即可

<div id="my-slider" class="slider-list"></div>

JS代码中使用一个render()的API来把HTML模块化,使用一个数组来存图片的路径

class Slider {
    constructor(id, opts = { images: [], cycle: 3000 }) {//id是容器的id,转来的轮播图的路径用一个数组进行保存,cycle是多少毫秒轮播一次默认3秒轮播一次
        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');//获取当前所有的li
        this.cycle = opts.cycle || 3000;
        this.sliderTo(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>`
    }
    registerPlugins(...plugins) {
        plugins.forEach(plugin => {
            const pluginController = document.createElement('div');
            pluginController.className = '.slider-list__plugin';
            pluginController.innerHTML = plugin.render(this.options.images);
            this.container.appendChild(pluginController);
            plugin.action(this);
        });
    }
    ...
}
// 圆点按钮插件
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 pluginPrevious = {
    render() {
        return `<a class="slide-list__previous"></a>`;
    },
    action(slider) {
        ...
    }
}
// 下一页插件
const pluginNext = {
    render() {
        return `<a class="slide-list__previous"></a>`;
    },
    action(slider) {
       ...
    }
}
const slider = new Slider('my-slider', { images: ['img/1.png', 'img/2.png', 'img/3.png', 'img/4.png', 'img/5.png'], cycle: 3000 });
slider.registerPlugins(/*pluginController, */pluginPrevious, pluginNext);//注册圆点, 上一页,下一页事件
slider.start();

通过这样HTML模块化后,如果我们注释掉圆点部分的插件,相其他部分的功能也不会受到影响,那么在HTML中相应的圆点也不会有,需要新的插件,我们重新写好插件直接往里面注册就行了

image.png

重构:组件框架

点击这里可以查看完整代码及效果

抽象

  • 将通用的组件模型抽象出来
    可以看到模板化中的registerPlugins(...plugins)、render()方法是通用的组件,于是我们将它们抽离出来放在Component类中,然后通过继承的方式将其继承该子类Slider,这样就能抽象出一个Component类,这个类体系相对完整,可以相当于是一个很小的组件框架

image.png

class Comonent {
    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 pluginController = document.createElement('div');
            pluginController.className = `.${name}__plugin`;
            pluginController.innerHTML = plugin.render(this.options.data);
            this.container.appendChild(pluginController);
            plugin.action(this);
        });
    }
    render(data) {
        /* abstract 在子类实现这个方法即可 */
        return '';
    }
}

子类的写法:

class Slider extends Comonent {
    constructor(id, opts = { name: 'slider-list', data: [], cycle: 3000 }) {//id是容器的id,传来的轮播图的路径用一个数组进行保存,cycle是多少毫秒轮播一次默认3秒轮播一次
        super(id, opts);
        this.items = this.container.querySelectorAll('.slider-list__item, .slider-list__item--selected');//获取当前所有的li
        this.cycle = opts.cycle || 3000;
        this.sliderTo(0);//默认第一张图片显示
    }

    render(data) {
        const content = data.map(image => `
            <li class="slider-list__item">
                <img src="${image}"/>
            </li>    
        `.trim());
        return `<ul>${content.join('')}</ul>`;
    }
    ...
   }

总结:

  • 组件设计的原则:封装性、正确性、扩展性、复用性
  • 实现组件的步骤:结构设计、展现效果、行为设计
  • 三次重构:
    • 插件化
    • 模板化
    • 抽象化(组件框架)

各司其职: 各司其职的博客链接
组件封装: 你的位置在这里~
过程抽象: 过程抽象的博客链接

  • 9
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值