Vue 组件库实践和设计

来源:互联网 发布:php magic quotes gpc 编辑:程序博客网 时间:2024/05/16 10:31

转载https://juejin.im/post/598965bd5188256da941872c

现在前端的快速发展,已经让组件这个模式变的格外重要。对于市面上的组件库,虽然能满足大部分的项目,但是一些小型细节方面和使用方面,或者UI库存在的一些bug,会让人很头疼。

那我们应该如何面对解决这些问题。俗话说自己动手丰衣足食。有些组件不用刻意去造。应该考虑如何去打造一个快速,兼容性好,功能齐全的组件库。

  1. 先到github上和一些大公司开源的组件库官网上去看看你所需组件库的demo例子,Prop和event暴露出来了哪些接口。

  2. 货比三家。别人分别用了哪些模式设计,哪种模式最简便,更合理。

  3. 一般成熟的UI库兼容性是经过大量测试和用户使用后没有问题后的结果。省去这一部分,进行样式的借鉴。

  4. 不要太急于进行组件的大量造轮子。因为一个人的战斗是有限的。根据需要和场景项目进行一个个定位,积少成多。

Crib-zk也是我个人目前针对自己项目的需求,额外进行的组件。虽然不能用于开源市场的使用,但是可以用于大家的学习。使用和学习是两种模式。会使用不代表你懂,一旦有些需求不在开源项目组件的范围之内。此时又不能清楚内部的原理,就会措手不及。接下来进行一步一步分析。

大纲:分析组件

  1. alert 插件/组件

  2. backtop 组件

  3. sms-countDown组件(短信倒计时)

  4. search 组件

  5. infinitscroll 组件

  6. actionSheet 组件

  7. accordion 组件(手风琴)

注意 : 看这篇文章最好结合我github上发布的组件,进行比对式观看。

github:github.com/ziksang/cri…

demo:http://39.108.49.237:3000/dist/

一、alert组件/插件

如果在alert这种弹出式组件里,首先要加一些背景layout的背景层动画化,可以简称为dialog动化,进行一个包裹。

对于alert组件/插件的区别使用性是在那里?

一般来说,先会定义一个.vue文件的alert模板。

enter image description hereenter image description here

<template>    <div class="vux-alert">        <x-dialog :value='alertShow' :isClose='false'>            <div class="crib-confirm_hd" :style='[titleStyle]'>                <strong class="crib-confirm_title">{{title}}</strong>            </div>            <div class="crib-confirm_bd">                <slot>                    <div v-html="content"></div>                </slot>            </div>            <div class="crib-confirm_ft">                <a href="javascript:;" class="crib-confirm_btn crib-confirm_btn_primary" @click="_onSubmit" :style='[buttonStyle]'>{{buttonText}}</a>            </div>        </x-dialog>    </div></template>

仔细看上面模板。

  1. 我们发现唯一特别的是对content体中的定义了一个solt。这个slot就是组件模式和插件模式的区分。如果我们想对slot里面定义的是一个额外的展示模板或其它组件插入的话,此时只能用组件模式。

  2. 如果只是我们对content这个数据进填充的话,插件模式也是最方便的。

props接口的暴露

  1. value 显示消息

  2. title (标题)

  3. content 内容最好支持html格式

  4. buttonText底部的按钮文案

  5. titleStyle 标题样式

  6. buttonStyle button样式

event接口暴露

  1. onsubmit 点击时向外暴露事件

  2. onshow 显示时向外暴露的显示事件

  3. onhide 显示时向暴露事件

 data() {        return {            alertShow: this.value        }    },    watch: {        value(val) {            this.alertShow = val        }    },    methods: {        _onSubmit() {            this.alertShow = false            this.$emit('update:value', false)            this.$emit('on-submit')        }    }

对于value这个值来说,可以用.sync来进行简便的操作。不需要通过$emit来进行通知。在声明组件的时候用$on去进行监听事件,省去了开发者这一步的事。

插件模式

首先要把原本的alert的.vue的模板给引处进来,然后用Vue.extend继承一下。

  $vm = new Alert({                el: document.createElement('div')            })

我们自己要手动进行一个创建挂载点。

//此方法是用来把confirm上的prop属性合并到调用时的参数上const mergeOptions = function($vm,options) {    //声明一个默认的对象,就是comfirm上props属性的default的值    const defaults = {}    //循环confirm属性上的props值    for (let i in $vm.$options.props){       //不把value的值算上去,显示改变通过watch或者改变data代理的属性上去监听       if(i !== 'value'){          defaults[i] = $vm.$options.props[i].default       }    }    //把confrim组件原本的值和插件传入的options合并    const _options = Object.assign({},defaults,options)    //把confirm组件生成的实列对象再次替换成合并的属性    for(let i in _options) {        $vm[i] = _options[i]    }}

同时要把value显示操作的默认定义的属性除外,进行自己定义后覆盖默认属性,进行显示。

$watcher = $vm.$watch('alertShow', (val) => {                    if (!val && options && options.onHide) {                        options.onHide()                    }                })

同时对alertshow进行监听,当点击submit的时候会自己动触发事件,然后会改变alertshow的值,然后进行你所想要的操作。

二、backtop 组件

对于backtop组件的话,要理解几点属性。

  1. scrollTop

  2. offsetHeight

let offsetTop = this.scrollview == window ? document.body.scrollTop : this.scrollview.scrollTop            let offsetHeight = this.scrollview == window ? document.body.offsetHeight : this.scrollview.offsetHeight

scrollTop 是距离顶部的高度。

offsetHeight 元素的高度包括边框。

那如何去判断什么时候显示返回顶部按钮呢?

this.show = offsetTop >= offsetHeight / 2;

只要通过滚动的高度大于滚动元素的高度/2来进行一个适配是最好的。

对于如何进行那方面优化。

可以进行函数节流。节流是个什么?因为进行滚动监听的时候,scroll事件触发的太频繁了。这会影响到整个性能的问题。如果对于上下滚动也要频繁监听。用节点,不适用于防抖操作。

 throttle(func, wait) {            var context, args;            var previous = 0;            return function () {                var now = +new Date();                context = this;                args = arguments;                if (now - previous > wait) {                    func.apply(context, args);                    previous = now;                }            }        },

通过时间戳来进行对比,来进行函数节流。但是有一点需要注意,在节流的同时,不要节流的时节太长。因为mobile上面节流滚动的话,有一个自行滑动的时长。

const getScrollview = function (el) {    //拿到当前节点    let currentNode = el;    //如果有节点,并且节点不等于html ,body 并且节点类型是元素节点    while (currentNode && currentNode.tagName !== 'HTML' && currentNode.tagName !== 'BODY' && currentNode.nodeType === 1) {       //拿到节点的overflowy的属性        let overflowY = document.defaultView.getComputedStyle(currentNode).overflowY;        //如果此时属性是scroll或者atuo 就返回此节点        if (overflowY === 'scroll' || overflowY === 'auto') {            return currentNode;        }        //否则就继续向父节点上找        currentNode = currentNode.parentNode;    }    //一但while语句为false的时候就直接返回window对像    return window;};export {getScrollview}

在外层要进行一个包裹,通overflow属性向来进行推测, 是全局滚动还是window 下的滚动,通过while来进行判断递归,来查找所对应的元素 。

三、sms-countDown 短信倒计时组件

enter image description hereenter image description here

对短信倒计时的认知

对于短信倒计时最重要的一点就是从父组件向sms组件通知倒计时开始的一个prop参数,用start替代。

  watch : {        start (value) {          if(value === true){            this.countDown()          }        }      }

同时对start在内部进行监听。一旦从外部传入开始的时候,则内部进行倒计时。

countDown () {            this.myTime = 60;            let time = setInterval(()=>{                this.myTime --;                if(this.myTime === 0){                  this.$emit('update:start', false);                  this.myTime = this.initText;                  this.flag = true;                  clearInterval(time)                }            },100)        }

在这里同样要进行一个倒计时停止之后的向外通知。还是用.sync的双向绑定方法,用于简便操作。

在对于第一次倒计时和第二次倒计时的时候,也要对文案这方面进行一个设定。

 firstCkText : {            type : String,            default : ''          },          secondCKText : {            type : String,            default : '重新获取'          },

第一次点击和第二次点击的按钮,也是对主要的文案的一种设计,所以对文案的变化也是要很关注的。

四、search组件

对于search组件通常能想到哪些对应的功能和想法呢?比如首次进来的时候,要进行自动获取Input的焦点。同时要向外面暴露是否要获取Input获点事的Prop :autoFocus。同时也要注意,一定要在Mounted的时候才能拿 到dom元素。

  mounted() {        this.autoFocus && this.$refs.input.focus()    }

一般想知道input里面的value值是否改动的时候,通常都会用keydown或者是keyup事件。但是这里不需要,可以时时把value的值给暴露出去,让外层父组件可以去进行watch监听来进行进所需要的事件操作。

watch: {        inputValue (val) {           if(val == ''){               this.value = ""           }        },        value: {            handler(val, oldvalue) {                //当值改变的时候,触发事件                this.$emit('update:inputValue', val)                this.$emit('change-val')            },            immediate: true        }    },

同时这里用到了.sync ,在页面一加载的时候,立马执行了。immediate使得value这个值立马值行了监听。

五、infinite-scroll 组件 (无限滚动组件)

无限滚动最关键的三个地方。第一滚动动底部触发事件;第二如果有二次加载则显示 loading;第三如何没有二次加载则结束文案。

import { getScrollview } from '../../libs/getScrollview.js';

这个不用说,继续寻找需要滚动范围的元素 。

 data() {        return {            isLoading: false, //是否正在加载            isDone: false,  //是否加载完毕        }    },

data里面进行之前说的两种模式的状态进行定义。往下看,这一处定义之后对后面有什么好处。

 this.$on('Load', () => {                this.isLoading = false;            });            this.$on('loadDone', () => {                    this.isLoading = false;                    this.isDone = true;            });

需要监听两个事件:

  1. 二次加载load事件。一旦进行二次加载的时候,马上进行isloading等于false 防止重复加载。

  2. 通过loadDone对是否监听完毕。如果加载完毕的话,同样的关闭isloading 对isDone进行true的设置。

isloading和isDone分别对应的那个html 的template部分。

 <div class="list-donetip" v-show='!isLoading&& isDone'>            <slot name='doneTip'>没有更多数据了</slot>        </div>        <div class="list-loading" v-show='isLoading'>            <slot name='loadingTip'>加载中...</slot>        </div>

当isloading 为true的时候显示“加载中... ”当isloading 为false的时候,isDone为true的时候才显示 “没有更多数据”,这也是一个标准的无限滚动。

什么时候对isloading和isDone设置为true?

 scrollHandler() {            if (this.isLoading || this.isDone) return;            let baseHeight = this.scrollview == window ? document.body.offsetHeight : this.scrollview.offsetHeight            let moreHeight = this.scrollview == window ? document.body.scrollHeight : this.scrollview.scrollHeight;            let scrollTop = this.scrollview == window ? document.body.scrollTop : this.scrollview.scrollTop            if (baseHeight + scrollTop + this.distance > moreHeight) {                this.isLoading = true;                this.onInfinite()            }            if (!this.scrollview) {                console.warn('Can\'t find the scrollview!');                return;            }        },

当滚动到底部的时候,对isloading进行为true的设置。外部组件可以调用onInfinite,进行ajax请求等操作。

在外部如何调用呢?

this.$refs.infinite.$emit('loadDone')

对组件进行 ref的设置,然后进行触发loadDone或者load。

比对饿么的组件,它使用的是指令的模式,内部实现还是太复杂。

如何进行一个优化

这里就用到了函数防抖,同上面不用函数节流,用防抖。防抖跟节流的有什么区别?防抖比较更节省性能。如果我们在设置的时间内一直滑动,则不会进行加载,只有滑动到指定的地方,则可以进行检测,通过定时器来实现。

 debounce  (func, wait) {            var timeout;            return function () {                var context = this;                var args = arguments;                clearTimeout(timeout)                timeout = setTimeout(function () {                    func.apply(context, args)                }, wait);            }        },

六、actionSheet 组件

enter image description hereenter image description here

actionSheet 这里亮点就是巧用了。call方法来改变了this的指向。这个有什么好处?往下面看。

prpps : model 我通过Model这个数据进行递,把文案的改变,点击后所执行的方法一并封装到Model数据里来进行操作。

如果在父组件引入这个组件的时候,看下面代码。

 model: [                { name: this.name, method(name, index) { location.href = 'tel:110' } }            ],

如果进行this.指向的话,指向的是父组件。这里就不能直接在data里面声明了。如果是异步的话,只有在ajax请求的异步里进行声明,把值传入,是如何做的呢?

 ff (item,index,method) {             this.$emit('update:show', false)             method.call(this.$parent,item,index)        }

在这里通过.call来把this.的指向到父组件,就能成功的方便的调用了。

七、accordion 手风琴组件

enter image description hereenter image description here

对accordion组件要进行定义两个组件合并成一个组件的模式。

  1. 一个最外层的包裹组件。

  2. 第二个是每一个item的组件。

每说 accordion-item里面的组件,通过

this.height = (this.show ? this.$refs.content.offsetHeight: 0) + 'px';

如果需要显示的话,让每一个item的元素来计算高度,展现出来。

通过_uid来进行 每个item的识别。

  methods : {           open(uid) {               this.$children.forEach (item => {                   console.log(item._uid)                   if(item._uid == uid){                       item.show = !item.show                   }else{                       if(!this.repeat){                           item.show = false                           item.height = 0;                       }                   }               })           }       }

能够收起的是那个item组件,则向收起的那个item组件进行一个传递。本质上通过index找到子组件对应的项也可以实现。因为_uid是唯一的。这一步也是省了一些简便的操作。

在这里把一些突出的组件,来开阔我们的思想,来进行一其它组件的封装 ,也可以基于这些组件对自己所需要的项目根据不同的需求来封装。

最后,尤大说了一句话,我最喜欢的就是看别人代码。记住这句话,你的组件能写的又快又好。

这是我最近在Gitchat上交流的一篇文章。如果大家对Vue感兴趣的话,可以加入我的Vue讨论微信群。有什么问题可以在群里交流。



原创粉丝点击