美文网首页
JS开发自己的组件《丝滑球》

JS开发自己的组件《丝滑球》

作者: 十年之后_b94a | 来源:发表于2020-08-11 12:02 被阅读0次

    1、前言

    在我访问手机版起点网站的时候,发现首页有个最近阅读的小球,于是想开发一个小型插件吧,目标是引入插件,就能实现丝滑般的滑动小球。

    image.png

    2、开发前准备=>搭建环境

    我们使用rollup脚手架开发库

    rollup:核心脚手架
    @babel/preset-env:解析es6语法
    @babel/core:使用babel模块
    rollup-plugin-babel:使rollup能过使用babel模块
    rollup-plugin-serve:使用rollup-serve启动一个微服务
    cross-env:环境变量

    //第一步
    npm init -y
    //第二步
    cnpm i rollup @babel/preset @babel/core rollup-plugin-babel rollup-plugin-serve cross-env -D
    

    2-1、创建基本文件配置文件

    1、创建.babelrc
    2、创建rollup.config.js
    3、创建src目录
    4、创建public目录=>用来测试插件
    5、在刚创建的public目录创建index.html
    6、在index.html引用我们打包后的js地址
    <script src="/dist/bundle.js"></script>
    
    //.babelrc
    {
        "presets": [
            "@babel/preset-env"
        ]
    }
    

    2-2、配置rollup启动环境

    //rollup.config.js
    import babel from 'rollup-plugin-babel';
    import serve from 'rollup-plugin-serve';
    
    export default {
        input: './src/index.js',//入口
        output: {
          file: 'dist/bundle.js',//出口
          name : 'SilkBall',//打包后导出的全局变量名
          format: 'umd',//模块规范
          sourcemap : true//开启源代码调试
        },
        plugins : [
            babel({
                exclude:'node_modules/**' //忽略node_modules文件夹
            }),
            process.env.ENV === 'development'?serve({
                open : true,//项目启动自动打开网页
                openPage : '/public/index.html',//打开的网页路径
                port : 3000, //端口
                contentBase : ""
            }):null //如果是开发模式我们开启服务
        ]
      };
    

    2-3、配置package.json启动命令

    "scripts": {
        "serve": "cross-env ENV=development rollup -c -w",
        "build": "rollup -c"
      }
    //-c 使用配置文件打包
    //-w实时打包
    

    2-4、配置.babelrc

    {
        "presets": [
            "@babel/preset-env"
        ]
    }
    

    3、开始编写核心代码

    首先思考,我们的目标是使元素可以丝滑般滑动,那么我们现在可知,用户的参数应该包含以下

    1、滑动的元素 必填
    2、可选配置
    -- 2-1、限制的滑动范围,默认为整个页面,但是可选择父元素内滑动
    -- 2-2、是否开启磁吸,默认开启:即当滑动至中间范围,是否自动贴边
    -- 2-3、限制滑动小球的边界,即完美贴边还是距离边界一定的距离
    -- 2-4、是否开启记录小球的位置,当有历史记录时新进页面默认小球为上一次结束时位置,默认关闭
    3、提供给用户三个监听的函数
    -- 3-1、滑动开始,并返回滑动小球开始时位置
    -- 3-2、滑动过程中,并返回小球实时位置
    -- 3-3、滑动结束,并返回结束后小球位置
    

    //定义以上默认可选配置

    //默认配置
    export const DEFAULT_OPTIONS = {
        rangeBody : true, //限制的范围 默认为body
        magnet : true, //开启磁吸
        direction  : 'x',//磁吸方向
        margin : 0,//开启磁吸后贴边的边距
        history : false,//关闭记录历史位置
        speed : 500,//惯性速度
        engine : 'js',//磁吸动画模式  js动画/css动画
        cssCubic : 'cubic-bezier(0.21, 1.93, 0.53, 0.64)', //贝塞尔曲线
        jsCubic : 'Bounce_easeOut'
    };
    

    3-1、添加监听事件

    我们优先使用touch事件,当然也应该兼容mouse事件,因为PC端支持鼠标事件,不支持触摸事件
    当绑定监听事件的时候,我们方便后续解绑事件,我们使用handleEvent

    //判断能否使用touch事件
    hasTouch = inBrowser && 'ontouchstart' in window;
    //添加事件
    function addEvent(el,type,fn){
        el.addEventListener(type, fn, {passive: false, capture: false})
    }
    SilkBall.prototype._addDOMEvents = function(){
            if(hasTouch){
                addEvent(this.$el,'touchstart',this);
                addEvent(this.$el,'touchmove',this);
                addEvent(this.$el,'touchcancel',this);
                addEvent(this.$el,'touchend',this);
            }
            else{
                addEvent(this.$el,'mousedown',this);
                addEvent(this.$el,'mousemove',this);
                addEvent(this.$el,'mousecancel',this);
                addEvent(this.$el,'mouseup',this);
            }
        }
    
    //然后我们在原型上添加handleEvent发放
    SilkBall.prototype.handleEvent = function(){
    switch (e.type) {
          case 'touchstart':
          case 'mousedown':
            同一个处理方法
            break
          case 'touchmove':
          case 'mousemove':
            同一个处理方法
            break
          case 'touchend':
          case 'mouseup':
          case 'touchcancel':
          case 'mousecancel':
            同一个处理方法
            break;
    }
    

    3-2、核心移动方法改变元素的transform属性

    SilkBall.prototype._translate = function (x, y) {
            if (isUndef(x) || isUndef(y)) throwError('moving distance cannot be empty!');
            x = Math.round(1000 * x) / 1000;
            y = Math.round(1000 * y) / 1000;
            this.$el.style.transform = `translate3d(${x}px,${y}px,0px)`;
            this.$el.style.webkitTransform = `translate3d(${x}px,${y}px,0px)`;
        }
    

    3-3、分析小球脱手后的运动轨迹及速度以及计算小球的位置

    首先我们是使用translate改变小球的位置,而不是使用position的定位,所以使用offset获取球的偏移是不可取的,使用getBoundingClientRect

    获取脱手后惯性速度
    滑动距离 = Math.sqrt(Math.pow(结束X-起始X,2)+Math.pow(结束Y-起始Y,2))
    不知道怎么算的可以翻翻初中课本~
    惯性速度=滑动距离/(松手后时间-起始时间) * 15
    想想为什么要*15?和下面的道理是一样的 在实际滑动中 单单距离/时间 这个速度是很短的 几乎都是在0.0~以内,如果以该速度进行惯性递减那么几乎都看不到惯性滑动了

    image.png
    重点:如果滑动距离足够大或者时间间隔足够小,那我们的惯性速度会很大,然后我们小球脱手后依靠惯性速度递减那我们的小球就像吃了炫迈一样,"根本停不下来"会很久才能停下来这样肯定不可取。
    speed = Math.min(10,惯性速度)我们规定如果惯性速度大于10,那我们就取10

    分析脱手后小球的运动轨迹

    image.png
    来思考下,为什么需要重置为1?
    因为如果第一次滑动我们惯性够大使他遇到边界反弹了,那么运动方向为-1了,而第二次开始惯性滑动我们拿上次的-1会导致本来是往惯性方向滑动,但是还没遇到边界就往反方向滑动
    按照图上的思路 我们拿到公式:惯性运动量 = 当前小球的位置 + 运动方向 * 速度
    小球位置 = 当前小球位置 + 惯性运动量
    但随之而来的又有一个问题了
    如果我们只改变一个方向即:我们始终保持Y轴的垂直方向滑动,不改变X轴的方向,那按照我们的理解公式 X轴和Y轴都会增加偏移量,显然不可取
    那必然和滑动的变化量有关
    如果公式:惯性运动量 = 当前小球的位置 + 运动方向 * 速度 * 轴向移动距离
    那肯定爆炸惯性运动飞快
    最终公式:惯性运动量 = 当前小球的位置 + 运动方向 * 速度 * 轴向移动距离 * 0.01
    这个0.01值我们可以提供个默认值然后让用户控制,值越大惯性速度越大

    3-4、磁吸

    首先磁吸贴边的动画效果
    1、使用css3transition的动画效果 来实现,相对简单
    2、使用JS算法实现动画效果,并且需要配合动画来实现
    这两种我们都实现:由用户选择模式。

    3-4-1、边界的位置

    因为用户参数有两个是决定边界因素的:
    1、是否在全屏范围内移动
    2、距离边距margin 参数

    let parNodeReac = getRect(getParentNode(this.$el));
    this.boundMargin = this.$options.rangeBody ? {
                left : 0 + this.$options.margin,
                top : 0 + this.$options.margin,
                right : window.innerWidth - this.$options.margin,
                bottom : window.innerHeight - this.$options.margin
            } : {
                left : parNodeReac.left + this.$options.margin,
                top : parNodeReac.top + this.$options.margin,
                right : parNodeReac.right - this.$options.margin,
                bottom : parNodeReac.bottom - this.$options.margin
            };
    

    3-4-2、css磁吸

    虽然我们有参数direction来决定磁吸的水平、垂直方向、但是我们必须判断球的位置在水平/垂直的那一半上
    假设我们现在设定在水平磁吸
    判断公式:小球当前左侧位置 - 边界左侧的位置 + 球宽度一半 <= (边界右侧-边界左侧) / 2

    第一步为滑动元素加上Transition
    当然在滑动开始时需要清除Transition
    if(this.$options.direction === 'x'){
                let bean = magneTdirection(bound.left - this.boundMargin.left + bound.width / 2,this.boundMargin.right - this.boundMargin.left)
                if(bean){//贴左侧
                    this.moveOldX = this.boundMargin.left-this.elStartBound.left;
                    this._translate(this.moveOldX, this.moveOldY)
                }
                else{//右侧
                    this.moveOldX = this.boundMargin.right - this.elStartBound.left - this.elStartBound.width;
                    this._translate(this.moveOldX, this.moveOldY)
                }
            }else{
                let bean = magneTdirection(bound.top - this.boundMargin.top + bound.height / 2,this.boundMargin.bottom - this.boundMargin.top)
                if(bean){//顶部
                    this.moveOldY = this.boundMargin.top-this.elStartBound.top;
                    this._translate(this.moveOldX, this.moveOldY)
                }
                else{//底部
                    this.moveOldY = this.boundMargin.bottom - this.elStartBound.top - this.elStartBound.width;
                    this._translate(this.moveOldX, this.moveOldY)
                }
            }
    

    3-4-3、js动画磁吸

    js动画的参数
    t:当前时间
    b :初始值
    c :变化量
    d:持续时间
    我们定义当前开始的时间为0
    持续时间(结束时间为30)
    初始值其实就是开始磁吸的时候小球的位置
    变化量 就是磁吸结束后小球应该在的位置
    然后判断边界
    再然后使用`requestAnimationFrame`动画让开始的时间累加
    如果时间小于结束时间一直执行否则结束执行
    

    当逻辑处理完成之后还有一个不得不处理的问题

    当在实现动画过程如果用户重新抓取小球,小球应当立即停止动画
    其实很简单,滑动开始时设置一个开关,然后滑动结束后开启这个开关,执行磁吸动画的时候判断这个开关就行

    3-5、处理回调函数

    我们在开始时就打算处理三个回调函数
    1、touchStart滑动开始时让用户可以监听此函数
    2、touchMove滑动时监听的函数,并且当惯性的时候应该加入监听、磁吸
    3、touchEnd滑动结束时监听的函数,这个比较特殊,因为我们有参数决定是否磁吸,开启磁吸 当磁吸结束后再返回当前坐标,否则滑动结束就返回。
    还需要考虑的是用户可能在多个地方监听同一个事件

    SilkBall.prototype.on = function(type,fn){
            if(!this._events[type]){
                this._events[type] = [];//数组是因为多个地方监听同一个函数
            }
            this._events[type].push(fn);
        }
        SilkBall.prototype.trigger = function(type){
            let events = this._events[type];
            if(!events) return
            events.forEach(element => {
                let event = element;
                event && event(...[].slice.call(arguments, 1))
            });
        }
    

    当这一步完成之后我们需要在函数中添加事件

    1、在开始的事件中添加

    this.trigger('touchStart',{
     x : this.boundX,//当前坐标
     y : this.boundY//当前坐标
    })
    

    、、、剩下两个亦相同

    3-6、处理是否保留历史位置

    这个没多大难度,最多当滑动结束的时候使用localStorage记录下位置,初始化的时候获取下赋值

    4、发布

    大家有兴趣可以来看看源码,点个赞再走~

    npm i silk-ball -S
    

    用法

    import SilkBall from 'silk-ball'
    let silkBall = new SilkBall(el,options);
    

    相关文章

      网友评论

          本文标题:JS开发自己的组件《丝滑球》

          本文链接:https://www.haomeiwen.com/subject/csybrktx.html