原生 JavaScript 实现滑动拖动验证

image.png

一般,咱们为了防止用户恶意提交表单,会让用户在提交前完成滑动拖动验证,有时候这也能起到一丝反爬的做用。javascript

实现滑动验证的方式固然不止一种,这里咱们直接使用原生 JavaScript 来实现。css

如今,你能够在 这里 看到完整的源码,欢迎 Star 哦😄。html

原生实现

原生 JavaScript 的实现,主要是经过监听鼠标事件来对 DOM 进行一系列的操做。java

滑块验证的结构主要分为四个部分:轨道、滑块、背景和文案,咱们可使用下面的 HTML 结构来表示。git

<div class="slide-track">
    <div class="slide-bg"></div>
    <div class="slide-block"></div>
    <p class="slide-text">请按住滑块,拖动到最右边</p>
</div>

基本思路就是咱们给滑块(.slide-block)添加相应的事件,在按下滑块时记录鼠标的当前位置并添加滑动事件,在滑动过程当中根据鼠标的移动来移动滑块的位置和增长背景元素(.slide-bg)的宽度,直到移动到轨道(.slide-track)的末端后,改变文案(.slide-text)来提示成功。github

样式

在开始写脚本以前能够先来完成一下它们的样式,这让滑块相关的部分看起来更好,也让后面的工做更愉快的进行。app

/* 样式的注意事项 */

样式的写法就不贴了,相信你们一看就懂,并且会有更好的实现。须要的话,也能够在 Github 上找到。ide

脚本

如今开始来实现脚本的内容,首先咱们对 document.querySelector 方法进行简单的封装以方便后续操做 DOM 元素。函数

function $(selectors) {
    return document.querySelector(selectors);
}

而后经过自定义的 _h 函数咱们能够很方便的建立上面的 HTML 结构,并添加到文档中。学习

function _h(tagName, propMap = {}, text) {
    const ele = document.createElement(tagName);
    Object.keys(propMap).forEach(prop => ele.setAttribute(prop, propMap[prop]));
    if (text) {
        ele.appendChild(document.createTextNode(text));
    }
    return ele;
}

class SlideUnlock {
    constructor(el = "body", options = {}) {
        this.$el = $(el)
        this.$$isSuccess = false
        this.$options = { // 默认配置
            tip: '请按住滑块,拖动到最右边',
            unlockText: '验证成功',
            duration: 500,
            ...options
        }
    }

    init() {
        this.$$root = _h("div", { class: "slide-track" }) // 轨道
        this.$$bg = this.$$root.appendChild(_h("div", { class: "slide-bg" }))
        this.$$block = this.$$root.appendChild(
            _h("div", { class: "slide-block" }) // 滑块
        )
        this.$$text = this.$$root.appendChild(
            _h("p", { class: "slide-text" }, this.$options.tip)
        )
        this.$el.insertBefore(this.$$root, this.$el.firstChild)
    }
}

在建立好 DOM 结构后,接下来为滑块添加鼠标按下的事件,在这个事件中咱们须要记录下鼠标的初始横坐标,以便后续和滑动过程当中的位置相比较,同时为其添加滑动事件。

class SlideUnlock {
    init() {
        /* ... */
        this.$$block.addEventListener(
            "mousedown",
            (this.$$handleMouseDown = this._handleMouseDown.bind(this)),
            false
        )
    }

    _handleMouseDown(e) {
        const downx = e.clientX

        e.target.addEventListener(
            "mousemove",
            (this.$$handleMouseMove = this._handleMouseMove.bind(this, downx)),
            false
        )
        e.preventDefault()
    }

    _handleMouseMove(downx, e) {}
}

在这里有点细节须要注意:

  • 首先,因为事件监听器中的 this 指向的是触发事件的元素,为此咱们在指定鼠标按下的监听器时为其绑定了 this,以便调用滑块实例属性和原型上的方法。
  • 其次,咱们在鼠标按下的监听器中添加了鼠标移动的监听器,若是在初始时同按下的监听器一块儿指定,那么会先执行鼠标移动的监听器,而此时并无记录鼠标的初始位置;

接下来咱们要实现滑动过程当中的主要逻辑:根据鼠标的移动实时地更新滑块的位置,并对一些临界位置进行处理。

_handleMouseMove(downx, e) {
    const info = this.$$block.getBoundingClientRect(),
        x = e.clientX,
        y = e.clientY,
        x1 = info.left,
        y1 = info.top,
        x2 = info.right,
        y2 = info.bottom,
        moveX = x - downx

    if (this.$$isSuccess) {
        return
    }
    if (moveX < 0) {
        return
    }
    if (x < x1 || x > x2 || y < y1 || y > y2) {
        // 当鼠标移开滑块时取消移动
        return
    }

    this.$$block.style.left = `${moveX}px` // 更新滑块的我i之
    this.$$bg.style.width = `${moveX}px` // 同步增大背景元素的宽度

    // 当滑块滑动的距离大于等于轨道除去滑块宽度后的距离时表示已经到达轨道的最右边了
    if (moveX >= this.$$root.offsetWidth - (x2 - x1)) {
        this.$$isSuccess = true
        this.$$text.textContent = "验证成功"
        this.$$text.style.cssText = `color: #fff; left: 0; right: ${this.$$block.offsetWidth}px`
        this.$$block.classList.add("success")
    }
}

这里的实现也很简单,惟一须要看一下的就是经过 getBoundingClientRect 来获取了滑块相对于视口的位置,而后根据鼠标所在的位置来判断鼠标是否在滑块上,若是不在则取消移动。

如今它已经能很好的滑动,并完成提示成功的基本功能了。可是,当咱们每次滑动到中间就取消,而后再次点击滑动时,就会致使重复的添加滑动事件,并且中途释放后,滑块就停在了当前位置,这显然不对。

解决的办法就是在添加鼠标按下事件的时候,同时也指定一个松开的事件,在这个事件的监听器中判断若是没有成功则取消以前绑定的滑动事件,并进行重置,为了看起来更友好,咱们还能够加上一点动画。

class SlideUnlock {
    init() {
        /* ... */
        document.addEventListener(
            "mouseup",
            (this.$$handleMouseUp = this._handleMouseUp.bind(this)),
            false
        )
    }

    _handleMouseDown(e) {
        /* ... */
        // 取消在手动滑动过程当中的动画效果
        this.$$bg.style.transition = ""
        this.$$block.style.transition = ""
        /* ... */
    }

    _handleMouseUp(e) {
        this.$$block.removeEventListener(
            "mousemove",
            this.$$handleMouseMove,
            false
        )

        if (this.$$isSuccess) {
            return
        }

        // 给重置过程添加动画效果
        this.$$bg.style.transition = "width 1s ease"
        this.$$block.style.transition = "left 1s ease"

        this.$$block.style.left = 0
        this.$$bg.style.width = 0
    }
}

目前为止,滑块已经能够在 PC 端正常的工做了,不过在移动端却并不理想。

为了它可以在移动端也能够很好的工做,咱们能够借助 touchstarttouchmovetouchend 等事件来完成。

绑定这些事件的时机和处理方式和以前的三个事件分别相对应,因此咱们新增两个方法(和 jQuery 的 on、off 方法很像)来更好的添加和移除事件。

function bindEvents(events, handler, element = $("body")) {
    events.split(" ").forEach(event => {
        element.addEventListener(event, handler, false)
    })
}

function unbindEvents(events, handler, element = $("body")) {
    events.split(" ").forEach(event => {
        element.removeEventListener(event, handler, false)
    })
}

根据这两个方法,咱们来稍微修改一下滑块中添加事件的代码。

class SlideUnlock {
    init() {
        /* ... */
        bindEvents(
            "mousedown touchstart",
            (this.$$handleMouseDown = this._handleMouseDown.bind(this)),
            this.$$block
        )
        bindEvents(
            "mouseup touchend",
            (this.$$handleMouseUp = this._handleMouseUp.bind(this)),
            document
        )
    }

    _handleMouseDown(e) {
        /* ... */
        if (e.cancelable) {
            e.preventDefault() // 阻止默认行为
        }

        /* ... */
        bindEvents(
            "mousemove touchmove",
            (this.$$handleMouseMove = this._handleMouseMove.bind(this, downx)),
            this.$$block
        )
    }

    handleMouseUp(e) {
        unbindEvents("mousemove touchmove", this.$$handleMouseMove, this.$$block)
        /* ... */
    }
}

另外,须要注意的是在移动端 touch 事件中获取 clientXclientY 时不能在事件对象上直接读取,而是在 event.changedTouches[0] 对象上取得。

如今,它已经可以同时在 PC 端和移动端上工做了,不过咱们还能对它进行一些优化,好比使用函数节流。

函数节流的实现方式有不少,这里列一下咱们在本次过程当中使用的方式。

utils.throttle = function(method, context = {}, delay = 4, ...outParams) {
  return function(...innerParams) {
    clearTimeout(context.$$tId)
    context.$$tId = setTimeout(function() {
      method.apply(context, [...outParams, ...innerParams])
    }, delay)
  }
}

而后用这个节流函数,来包装咱们移动时的处理函数,并根据实际状况作点调整。

除此以外,咱们还能够添加一个重置的方法,让它回到最初的状态,涉及到的内容也很简单,就是在成功的状态下从新设置样式和绑定事件。

reset() {
    if (!this.$$isSuccess) {
        return
    }
    this.$$isSuccess = false
    this.$$bg.style.cssText =
        `transition: width ${this.$options.duration}ms ease; width: 0;`
    this.$$block.style.cssText =
        `transition: left ${this.$options.duration}ms ease; left: 0;`
    this.$$text.style.cssText =
        `color: #5f5f5f; left: ${this.$$block.offsetWidth}px; right: 0;`
    this.$$text.textContent = this.$options.tip
    this.$$block.classList.remove("success")
    this._bindEvents()
}

好了,滑块的实现到这里就告一段落了,相信你们看到这里已经彻底明白了,甚至有更好的实现。

如何使用

你能够简单的在 HTML 页面中引入该脚本,而后根据本身的需求设置合适的样式;不过更好的方式是经过这样的思路,在项目中作一些改进(好比平滑降级)等处理。

接下来是一个简单的使用模板。

<body>
    <script src="slide-unlock/core.js"></script>
    <script>
        const slider = new SlideUnlock()
        slider.init()
    </script>
</body>

你能够在 这里 看见完整的使用方式和效果。

其它

这里写下的实现也只是提供一个思路,欢迎你们一块儿交流学习。

轻拍【滑稽】。。。

相关文章
相关标签/搜索