原生 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>

你能夠在 這裏 看見完整的使用方式和效果。

其它

這裏寫下的實現也只是提供一個思路,歡迎你們一塊兒交流學習。

輕拍【滑稽】。。。

相關文章
相關標籤/搜索