前端實現電子簽名(web、移動端)通用組件

前言

在現在的時代發展中,從以前的手寫簽名,逐漸衍生出了電子簽名。電子簽名和紙質手寫簽名一樣具有法律效應。電子簽名目前主要還是在需要個人確認的產品環節和司法類相關的產品上較多。

舉個常用的例子,大家都用過釘釘,釘釘上面就有電子簽名,相信大家這肯定是知道的。

那作爲前端的我們如何實現電子簽名呢?其實在html5中已經出現了一個重要級別的輔助標籤,是啥呢?那就是 canvas[2]。

什麼是canvas

Canvas(畫布)[3] 是在HTML5中新增的標籤用於在網頁實時生成圖像,並且可以操作圖像內容,基本上它是一個可以用JavaScript操作的位圖(bitmap)Canvas 對象表示一個 HTML 畫布元素 -。它沒有自己的行爲,但是定義了一個 API 支持腳本化客戶端繪圖操作。

大白話就是canvas是一個可以在上面通過javaScript畫圖的標籤,通過其提供的context(上下文)Api進行繪製,在這個過程中canvas充當畫布的角色。

<canvas></canvas>
複製代碼

如何使用

canvas給我們提供了很多的Api,供我們使用,我們只需要在body標籤中創建一個canvas標籤,在script標籤中拿到canvas這個標籤的節點,並創建context(上下文)就可以使用了。

...
<body>
    <canvas></canvas>
</body>
<script>
    // 獲取canvas 實例
    const canvas = document.querySelector('canvas')
    canvas.getContext('2d')
</script>
...
複製代碼

步入正題。

實現電子簽名

知道幾何的朋友都很清楚,線有點繪成,面由線繪成。

多點成線,多線成面。

所以我們實際只需要拿到當前觸摸的座標點,進行成線處理就可以了。

body中添加canvas標籤

在這裏我們不僅需要在在body中添加canvas標籤,我們還需要添加兩個按鈕,分別是取消保存(後面我們會用到)。

<body>
    <canvas></canvas>
    <div>
        <button>取消</button>
        <button>保存</button>
    </div>
</body>
複製代碼

添加文件

我這裏全程使用js進行樣式設置及添加。

// 配置內容
    const config = {
        width: 400, // 寬度
        height: 200, // 高度
        lineWidth: 5, // 線寬
        strokeStyle: 'red', // 線條顏色
        lineCap: 'round', // 設置線條兩端圓角
        lineJoin: 'round', // 線條交匯處圓角
    }
複製代碼

獲取canvas實例

這裏我們使用querySelector獲取canvas的 dom 實例,並設置樣式和創建上下文。

    // 獲取canvas 實例
    const canvas = document.querySelector('canvas')
    // 設置寬高
    canvas.width = config.width
    canvas.height = config.height
    // 設置一個邊框,方便我們查看及使用
    canvas.style.border = '1px solid #000'
    // 創建上下文
    const ctx = canvas.getContext('2d')
複製代碼

基礎設置

我們將canvas的填充色爲透明,並繪製填充一個矩形,作爲我們的畫布,如果不設置這個填充背景色,在我們初識渲染的時候是一個黑色背景,這也是它的一個默認色。

    // 設置填充背景色
    ctx.fillStyle = 'transparent'
    // 繪製填充矩形
    ctx.fillRect(
        0, // x 軸起始繪製位置
        0, // y 軸起始繪製位置
        config.width, // 寬度
        config.height // 高度
    );

複製代碼

上次繪製路徑保存

這裏我們需要聲明一個對象,用來記錄我們上一次繪製的路徑結束座標點及偏移量。

    // 保存上次繪製的 座標及偏移量
    const client = {
        offsetX: 0, // 偏移量
        offsetY: 0,
        endX: 0, // 座標
        endY: 0
    }
複製代碼

設備兼容

我們需要它不僅可以在web端使用,還需要在移動端使用,我們需要給它做設備兼容處理。我們通過調用navigator.userAgent獲取當前設備信息,進行正則匹配判斷。

    // 判斷是否爲移動端
    const mobileStatus = (/Mobile|Android|iPhone/i.test(navigator.userAgent))

複製代碼

初始化

這裏我們在監聽鼠標按下(mousedown)(web 端)/觸摸開始(touchstart)的時候進行初始化,事件監聽採用addEventListener

    // 創建鼠標/手勢按下監聽器
    window.addEventListener(mobileStatus ? "touchstart" : "mousedown", init)

複製代碼

三元判斷說明: 這裏當mobileStatustrue時則表示爲移動端,反之則爲web端,後續使用到的三元依舊是這個意思。

聲明初始化方法

我們添加一個init方法作爲監聽鼠標按下/觸摸開始的回調方法。

這裏我們需要獲取到當前鼠標按下/觸摸開始的偏移量和座標,進行起始點繪製。

Tips:web端可以直接通過event中取到,而移動端則需要在event.changedTouches[0]中取到。

這裏我們在初始化後再監聽鼠標的移動。

    // 初始化
    const init = event ={
        // 獲取偏移量及座標
        const { offsetX, offsetY, pageX, pageY } = mobileStatus ? event.changedTouches[0] : event 

        // 修改上次的偏移量及座標
        client.offsetX = offsetX
        client.offsetY = offsetY
        client.endX = pageX
        client.endY = pageY

        // 清除以上一次 beginPath 之後的所有路徑,進行繪製
        ctx.beginPath()

        // 根據配置文件設置進行相應配置
        ctx.lineWidth = config.lineWidth
        ctx.strokeStyle = config.strokeStyle
        ctx.lineCap = config.lineCap
        ctx.lineJoin = config.lineJoin

        // 設置畫線起始點位
        ctx.moveTo(client.endX, client.endY)

        // 監聽 鼠標移動或手勢移動
        window.addEventListener(mobileStatus ? "touchmove" : "mousemove", draw)
    }
複製代碼

繪製

這裏我們添加繪製draw方法,作爲監聽鼠標移動/觸摸移動的回調方法。

    // 繪製
    const draw = event ={
        // 獲取當前座標點位
        const { pageX, pageY } = mobileStatus ? event.changedTouches[0] : event
        // 修改最後一次繪製的座標點
        client.endX = pageX
        client.endY = pageY

        // 根據座標點位移動添加線條
        ctx.lineTo(pageX , pageY )

        // 繪製
        ctx.stroke()
    }
複製代碼

結束繪製

添加了監聽鼠標移動/觸摸移動我們一定要記得取消監聽並結束繪製,不然的話它會一直監聽並繪製的。

這裏我們創建一個cloaseDraw方法作爲鼠標彈起/結束觸摸的回調方法來結束繪製並移除鼠標移動/觸摸移動的監聽。

canvas結束繪製則需要調用closePath()讓其結束繪製

    // 結束繪製
    const cloaseDraw = () ={
        // 結束繪製
        ctx.closePath()
        // 移除鼠標移動或手勢移動監聽器
        window.removeEventListener("mousemove", draw)
    }
複製代碼

添加結束回調監聽器

    // 創建鼠標/手勢 彈起/離開 監聽器
    window.addEventListener(mobileStatus ? "touchend" :"mouseup", cloaseDraw)
    
複製代碼

ok,現在我們的電子簽名功能還差一丟丟可以實現完了,現在已經可以正常的簽名了。

我們來看一下效果:

取消功能 / 清空畫布

我們在剛開始創建的那兩個按鈕開始排上用場了。

這裏我們創建一個cancel的方法作爲取消並清空畫布使用

    // 取消-清空畫布
    const cancel = () ={
        // 清空當前畫布上的所有繪製內容
        ctx.clearRect(0, 0, config.width, config.height)
    }
複製代碼

然後我們將這個方法和取消按鈕進行綁定

     <button onclick="cancel()">取消</button>
複製代碼

保存功能

這裏我們創建一個save的方法作爲保存畫布上的內容使用。

將畫布上的內容保存爲圖片/文件的方法有很多,比較常見的是blobtoDataURL這兩種方案,但toDataURL這哥們沒blob強,適配也不咋滴。所以我們這裏採用a標籤 ➕ blob方案實現圖片的保存下載。

    // 保存-將畫布內容保存爲圖片
    const save = () ={
        // 將canvas上的內容轉成blob流
        canvas.toBlob(blob ={
            // 獲取當前時間並轉成字符串,用來當做文件名
            const date = Date.now().toString()
            // 創建一個 a 標籤
            const a = document.createElement('a')
            // 設置 a 標籤的下載文件名
            a.download = `${date}.png`
            // 設置 a 標籤的跳轉路徑爲 文件流地址
            a.href = URL.createObjectURL(blob)
            // 手動觸發 a 標籤的點擊事件
            a.click()
            // 移除 a 標籤
            a.remove()
        })
    }
複製代碼

然後我們將這個方法和保存按鈕進行綁定

    <button onclick="save()">保存</button>
複製代碼

我們將剛剛繪製的內容進行保存,點擊保存按鈕,就會進行下載保存

完整代碼

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta >
    <title>Document</title>
    <style>
        * {
            margin: 0;
            padding: 0;
        }
    </style>
</head>
<body>
    <canvas></canvas>
    <div>
        <button onclick="cancel()">取消</button>
        <button onclick="save()">保存</button>
    </div>
</body>
<script>
    // 配置內容
    const config = {
        width: 400, // 寬度
        height: 200, // 高度
        lineWidth: 5, // 線寬
        strokeStyle: 'red', // 線條顏色
        lineCap: 'round', // 設置線條兩端圓角
        lineJoin: 'round', // 線條交匯處圓角
    }

    // 獲取canvas 實例
    const canvas = document.querySelector('canvas')
    // 設置寬高
    canvas.width = config.width
    canvas.height = config.height
    // 設置一個邊框
    canvas.style.border = '1px solid #000'
    // 創建上下文
    const ctx = canvas.getContext('2d')

    // 設置填充背景色
    ctx.fillStyle = 'transparent'
    // 繪製填充矩形
    ctx.fillRect(
        0, // x 軸起始繪製位置
        0, // y 軸起始繪製位置
        config.width, // 寬度
        config.height // 高度
    );

    // 保存上次繪製的 座標及偏移量
    const client = {
        offsetX: 0, // 偏移量
        offsetY: 0,
        endX: 0, // 座標
        endY: 0
    }

    // 判斷是否爲移動端
    const mobileStatus = (/Mobile|Android|iPhone/i.test(navigator.userAgent))

    // 初始化
    const init = event ={
        // 獲取偏移量及座標
        const { offsetX, offsetY, pageX, pageY } = mobileStatus ? event.changedTouches[0] : event 

        // 修改上次的偏移量及座標
        client.offsetX = offsetX
        client.offsetY = offsetY
        client.endX = pageX
        client.endY = pageY

        // 清除以上一次 beginPath 之後的所有路徑,進行繪製
        ctx.beginPath()
        // 根據配置文件設置相應配置
        ctx.lineWidth = config.lineWidth
        ctx.strokeStyle = config.strokeStyle
        ctx.lineCap = config.lineCap
        ctx.lineJoin = config.lineJoin
        // 設置畫線起始點位
        ctx.moveTo(client.endX, client.endY)
        // 監聽 鼠標移動或手勢移動
        window.addEventListener(mobileStatus ? "touchmove" : "mousemove", draw)
    }
    // 繪製
    const draw = event ={
        // 獲取當前座標點位
        const { pageX, pageY } = mobileStatus ? event.changedTouches[0] : event
        // 修改最後一次繪製的座標點
        client.endX = pageX
        client.endY = pageY

        // 根據座標點位移動添加線條
        ctx.lineTo(pageX , pageY )

        // 繪製
        ctx.stroke()
    }
    // 結束繪製
    const cloaseDraw = () ={
        // 結束繪製
        ctx.closePath()
        // 移除鼠標移動或手勢移動監聽器
        window.removeEventListener("mousemove", draw)
    }
    // 創建鼠標/手勢按下監聽器
    window.addEventListener(mobileStatus ? "touchstart" : "mousedown", init)
    // 創建鼠標/手勢 彈起/離開 監聽器
    window.addEventListener(mobileStatus ? "touchend" :"mouseup", cloaseDraw)
    
    // 取消-清空畫布
    const cancel = () ={
        // 清空當前畫布上的所有繪製內容
        ctx.clearRect(0, 0, config.width, config.height)
    }
    // 保存-將畫布內容保存爲圖片
    const save = () ={
        // 將canvas上的內容轉成blob流
        canvas.toBlob(blob ={
            // 獲取當前時間並轉成字符串,用來當做文件名
            const date = Date.now().toString()
            // 創建一個 a 標籤
            const a = document.createElement('a')
            // 設置 a 標籤的下載文件名
            a.download = `${date}.png`
            // 設置 a 標籤的跳轉路徑爲 文件流地址
            a.href = URL.createObjectURL(blob)
            // 手動觸發 a 標籤的點擊事件
            a.click()
            // 移除 a 標籤
            a.remove()
        })
    }
</script>
</html>
複製代碼

各內核和瀏覽器支持情況

Mozilla 程序從 Gecko 1.8 (Firefox 1.5 (en-US)[4]) 開始支持 <canvas>。它首先是由 Apple 引入的,用於 OS X Dashboard 和 Safari。Internet Explorer 從 IE9 開始支持<canvas> ,更舊版本的 IE 中,頁面可以通過引入 Google 的 Explorer Canvas[5] 項目中的腳本來獲得<canvas>支持。Google Chrome 和 Opera 9+ 也支持 <canvas>

小程序中提示

在小程序中我們如果需呀實現的話,也是同樣的原理哦,只是我們需要將創建實例和上下文Api進行修改,因爲小程序中是沒有dom,既然沒有dom,哪來的操作dom這個操作呢。

關於本文

作者:桃小瑞

https://juejin.cn/post/7174251833773752350

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/BhrxCSEkDYYxwOOHBjPoHg