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

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);
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,968评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,601评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,220评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,416评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,425评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,144评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,432评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,088评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,586评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,028评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,137评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,783评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,343评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,333评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,559评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,595评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,901评论 2 345