使用 rollup 打包一个原生 js + canvas 实现的移动端手势解锁功能组件2

简介: 使用 rollup 打包一个原生 js + canvas 实现的移动端手势解锁功能组件

代码实现


1、新建初始化项目

npm init -y

e3a8c13c936f4055a66966b12172978b.png


2、安装依赖

npm i rollup -D


ebf91542f65b4f71bd4fd4206a7dcea5.png

为了让库文件具有更好的兼容性,需要把ES6代码在打包的时候转义成ES5。

# 安装rollup插件包
npm install @rollup/plugin-babel -D
# 安装babel相关包
npm install @babel/core @babel/preset-env -D



4b65865a6ca144008562913bf6f79cb5.png


在根目录下新建 .babelrc 文件,并撰写如下内容:

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


需要对生产环境进行压缩

# 安装代码压缩插件
npm install rollup-plugin-terser -D

d2f101b82cf7475d823dd3f45a0b5faa.png



3、配置 rollup.config.js 文件

根路径添加 rollup.config.js 文件,配置如下:

// 用于es6转es5
import { babel } from '@rollup/plugin-babel';
// 用于代码压缩
import { terser } from 'rollup-plugin-terser';
const config = {
    input: "./src/index.js",
    output: [
        {
            file: './lib/kaimo-handlock-umd.js',
            format: 'umd',
            name: 'KaimoHandlock'
            // 当入口文件有export时,'umd'格式必须指定name
            // 这样,在通过<script>标签引入时,才能通过name访问到export的内容。
        },
        {
            file: './lib/kaimo-handlock-es.js',
            format: 'es'
        },
        {
            file: './lib/kaimo-handlock-cjs.js',
            format: 'cjs'
        }
    ],
    plugins: [
        babel({
            babelHelpers: 'bundled' // 建议显式配置此选项(即使使用其默认值),以便对如何将这些 babel 助手插入代码做出明智的决定。
        }),
        terser()
    ]
}
export default config;



4、添加默认的配置项

添加 config/config.default.js

export const defaultRecorderOptions = {
    container: null, // 创建canvas的容器,如果不填,自动在 body 上创建覆盖全屏的层
    autoRender: true, // 是否自动渲染
    dotNum: 4, // 圆点的数量: n x n
    defaultCircleColor: "#ddd", // 未选中的圆的颜色
    focusColor: '#33a06f',  //当前选中的圆的颜色
    bgColor: '#fff', // canvas背景颜色
    innerRadius: 16, // 圆点的内半径
    outerRadius: 42, // 圆点的外半径,focus 的时候显示
    touchRadius: 64, // 判定touch事件的圆半径
    minPoints: 4, // 最小允许的点数
}
export const defaultLockerOptions = {
    update: {
        beforeRepeat: function(){},
        afterRepeat: function(){}
    },
    check: {
        checked: function(){}
    }
}



5、添加绘制的工具方法

添加 utils/draw-utils.js

// 获取canvas 的坐标:canvas 显示大小缩放为实际大小的 50%。为了让图形在 Retina 屏上清晰
export function getCanvasPoint(canvas, x, y) {
    let rect = canvas.getBoundingClientRect();
    return {
        x: 2 * (x - rect.left),
        y: 2 * (y - rect.top)
    };
}
// 计算连点之间的距离
export function distance(p1, p2) {
    let x = p2.x - p1.x,
        y = p2.y - p1.y;
    return Math.sqrt(x * x + y * y);
}
// 画实心圆
export function drawSolidCircle(ctx, color, x, y, r) {
    ctx.fillStyle = color;
    ctx.beginPath();
    ctx.arc(x, y, r, 0, Math.PI * 2, true);
    ctx.closePath();
    ctx.fill();
}
// 画空心圆
export function drawHollowCircle(ctx, color, x, y, r) {
    ctx.strokeStyle = color;
    ctx.beginPath();
    ctx.arc(x, y, r, 0, Math.PI * 2, true);
    ctx.closePath();
    ctx.stroke();
}
// 画线段
export function drawLine(ctx, color, x1, y1, x2, y2) {
    ctx.strokeStyle = color;
    ctx.beginPath();
    ctx.moveTo(x1, y1);
    ctx.lineTo(x2, y2);
    ctx.stroke();
    ctx.closePath();
}



6、添加入口文件

添加 index.js

import Recorder from './core/recorder.js';
import Locker from './core/locker.js';
export {
  Recorder,
  Locker
}

7、添加核心文件 recorder.js

添加核心文件 core/recorder.js

// 获取默认配置
import {defaultRecorderOptions} from "../config/config.default.js";
// 绘制方法
import {
    distance,
    drawLine,
    drawHollowCircle,
    drawSolidCircle,
    getCanvasPoint
} from "../utils/draw-utils.js";
export default class Recorder{
    static get ERR_USER_CANCELED(){
        return '用户已经取消';
    }
    static get ERR_NO_TASK(){
        return '暂无任务可执行';
    }
    constructor(options) {
        this.options = Object.assign({}, defaultRecorderOptions, options);
        this.container = null; // 容器
        this.circleCanvas = null; // 画圆的 canvas
        this.lineCanvas = null; // 画固定线条 canvas
        this.moveCanvas = null; // 画不固定线条的 canvas
        this.circles = []; // dotNum x dotNum 个实心圆坐标相关数据
        this.recordingTask = null; // 记录任务
        // 是否自动渲染
        if(this.options.autoRender) {
            this.render();
        }
    }
    // 渲染方法
    render() {
        // 拿到容器
        this.container = this.options.container || document.createElement('div');
        // 拿到容器宽高
        let {width, height} = container.getBoundingClientRect();
        // 画圆的 canvas
        this.circleCanvas = document.createElement("canvas");
        // 设置 circleCanvas 的宽高一样
        this.circleCanvas.width = this.circleCanvas.height = 2 * Math.min(width, height);
        // 设置 circleCanvas 的样式支持 retina 屏
        Object.assign(this.circleCanvas.style, {
            position: 'absolute',
            top: '50%',
            left: '50%',
            transform: 'translate(-50%, -50%) scale(0.5)', 
        });
        /**
         * cloneNode(true):方法可创建指定的节点的精确拷贝、拷贝所有属性和值。
         * true:递归复制当前节点的所有子孙节点
         * 复制画圆的 canvas 属性给到 lineCanvas、moveCanvas
         * */ 
        this.lineCanvas = this.circleCanvas.cloneNode(true);
        this.moveCanvas = this.circleCanvas.cloneNode(true);
        // 将三个 canvas 添加到容器里
        container.appendChild(this.lineCanvas);
        container.appendChild(this.moveCanvas);
        container.appendChild(this.circleCanvas);
        // touchmove 事件在 Chrome 下默认是一个 Passive Event 需要传参 {passive: false},否则就不能 preventDefault。
        this.container.addEventListener('touchmove', evt => evt.preventDefault(), { passive: false });
        // 开始渲染时清除上一次记录
        this.clearPath();
    }
    // 负责在画布上清除上一次记录的结果
    clearPath() {
        // 如果没有画圆的 canvas,则重新渲染
        if(!this.circleCanvas){
            this.render()
        };
        // 获取三个 canvas 的上下文,宽度,还有配置项
        let {circleCanvas, lineCanvas, moveCanvas, options} = this,
            circleCtx = circleCanvas.getContext('2d'),
            lineCtx = lineCanvas.getContext('2d'),
            moveCtx = moveCanvas.getContext('2d'),
            width = circleCanvas.width,
            {dotNum, defaultCircleColor, innerRadius} = options;
        // 清除三个 canvas 画布
        circleCtx.clearRect(0, 0, width, width);
        lineCtx.clearRect(0, 0, width, width);
        moveCtx.clearRect(0, 0, width, width);
        // 绘制 dotNum x dotNum 个实心圆
        let range = Math.round(width / (dotNum + 1));
        let circles = [];
        for(let i = 1; i <= dotNum; i++){
            for(let j = 1; j <= dotNum; j++){
                let y = range * i, x = range * j;
                drawSolidCircle(circleCtx, defaultCircleColor, x, y, innerRadius);
                let circlePoint = {x, y};
                circlePoint.pos = [i, j];
                circles.push(circlePoint);
            }
        }
        this.circles = circles;
    }
    // 负责记录:它是一个异步的,因为不知道什么时候用户停止移动,这里我们返回一个 promise 对象回去,让用户决定停止移动的
    async record() {
        // 获取三个 canvas 的上下文,还有配置项
        let {circleCanvas, lineCanvas, moveCanvas, options} = this,
            circleCtx = circleCanvas.getContext('2d'),
            lineCtx = lineCanvas.getContext('2d'),
            moveCtx = moveCanvas.getContext('2d');
        // 记录激活的圆点
        let records = [];
        // touchstart、touchmove事件执行的方法,用于绘制激活状态
        let handler = evt => {
            // 每次touchstart时清除上一次记录的结果
            if(evt.type === "touchstart") {
                records = [];
                this.clearPath();
            }
            // 获取配置
            let {bgColor, focusColor, innerRadius, outerRadius, touchRadius} = options;
            // 通过 changedTouches 转换得倒移动点的坐标
            let {clientX, clientY} = evt.changedTouches[0],
                touchPoint = getCanvasPoint(moveCanvas, clientX, clientY);
            // 遍历之前存的圆点
            for(let i = 0; i < this.circles.length; i++){
                // 取出之前圆点的坐标
                let point = this.circles[i],
                    x0 = point.x,
                    y0 = point.y;
                // 判断圆点跟移动点的距离是否小于判定touch事件的圆半径
                if(distance(point, touchPoint) < touchRadius){
                    // 绘制白色的圆,半径为 outerRadius
                    drawSolidCircle(circleCtx, bgColor, x0, y0, outerRadius);
                    // 绘制激活色的圆,半径为 innerRadius
                    drawSolidCircle(circleCtx, focusColor, x0, y0, innerRadius);
                    // 绘制激活的空心圆,半径为 outerRadius
                    drawHollowCircle(circleCtx, focusColor, x0, y0, outerRadius);
                    // 如果 records 里面有圆点了,就跟最后一个连线
                    if(records.length){
                        let p2 = records[records.length - 1],
                            x1 = p2.x,
                            y1 = p2.y;
                        drawLine(lineCtx, focusColor, x0, y0, x1, y1);
                    }
                    // 将 circles 里圆点截取出来,push 到 records 里面
                    let circle = this.circles.splice(i, 1);
                    records.push(circle[0]);
                    // 找到之后就break
                    break;
                }
            }
            // 如果 records 里面有圆点了,就跟最后一个连线
            if(records.length){
                let point = records[records.length - 1],
                    x0 = point.x,
                    y0 = point.y,
                    x1 = touchPoint.x,
                    y1 = touchPoint.y;
                // 先清空画布
                moveCtx.clearRect(0, 0, moveCanvas.width, moveCanvas.height);
                // 画移动的线
                drawLine(moveCtx, focusColor, x0, y0, x1, y1);        
            }
        };
        // 监听touchstart、touchmove事件
        circleCanvas.addEventListener('touchstart', handler);
        circleCanvas.addEventListener('touchmove', handler);
        let recordingTask = {};
        let promise = new Promise((resolve, reject) => {
            // 给recordingTask添加取消的方法
            recordingTask.cancel = (res = {}) => {
                let promise = this.recordingTask.promise;
                res.err = res.err || Recorder.ERR_USER_CANCELED;
                circleCanvas.removeEventListener('touchstart', handler);
                circleCanvas.removeEventListener('touchmove', handler);
                document.removeEventListener('touchend', done);
                resolve(res);
                this.recordingTask = null;
                return promise;
            }
            let done = evt => {
                // 清空移动canvas的画布
                moveCtx.clearRect(0, 0, moveCanvas.width, moveCanvas.height);
                // 如果没有记录的点直接退出
                if(!records.length) return;
                // 移除事件
                circleCanvas.removeEventListener('touchstart', handler);
                circleCanvas.removeEventListener('touchmove', handler);
                document.removeEventListener('touchend', done);
                let err = null;
                // 如果记录的个数小于配置的最小允许的点数,就提示报错
                if(records.length < options.minPoints){
                    err = `连接点数至少需要${options.minPoints}个`;
                }
                // 把坐标转成字符串
                resolve({
                    err, 
                    records: records.map(o => o.pos.join('')).join('')
                });
                this.recordingTask = null;
            };
            // 监听 touchend 事件
            document.addEventListener('touchend', done);
        });
        recordingTask.promise = promise;
        this.recordingTask = recordingTask;
        return promise;
    }
    // 负责终止记录过程,同样也是返回一个promise
    async cancel() {
        // 如果有任务就执行任务里的 cancel 方法
        if(this.recordingTask){
            return this.recordingTask.cancel();
        }
        return Promise.resolve({err: Recorder.ERR_NO_TASK});
    }
}



8、添加核心文件 locker.js

添加核心文件 core/locker.js

import Recorder from './recorder.js';
// 获取默认配置
import {defaultLockerOptions} from "../config/config.default.js";
// Locker 继承 Recorder
export default class Locker extends Recorder{
    static get ERR_PASSWORD_MISMATCH(){
        return '密码不匹配';
    }
    constructor(options = {}) {
        options.update = Object.assign({}, defaultLockerOptions.update, options.update);
        options.check = Object.assign({}, defaultLockerOptions.check, options.check);
        super(options);
    }
    // 更新密码
    async update() {
        await this.cancel();
        // 拿到钩子方法 beforeRepeat afterRepeat
        let beforeRepeat = this.options.update.beforeRepeat, 
            afterRepeat = this.options.update.afterRepeat;
        // 第一次输入密码
        let first = await this.record();
        // 如果报错并且错误等于 ERR_USER_CANCELED 就return
        if(first.err && first.err === Locker.ERR_USER_CANCELED) {
            return Promise.resolve(first);
        }
        // 如果报错,return出去
        if(first.err){
            this.update();
            beforeRepeat.call(this, first);
            return Promise.resolve(first);   
        }
        // 执行重复前的钩子
        console.log("第一次密码:", first.records);
        beforeRepeat.call(this, first);
        // 第二次输入密码
        let second = await this.record();
        // 如果报错并且错误等于 ERR_USER_CANCELED 就return
        if(second.err && second.err === Locker.ERR_USER_CANCELED) {
            return Promise.resolve(second);
        }
        // 如果第二次密码没有错,并且第一次的密码不等于第二次的就报错不匹配
        if(!second.err && first.records !== second.records){
            second.err = Locker.ERR_PASSWORD_MISMATCH;
        }
        this.update();
        // 执行重复后的钩子
        console.log("第二次密码:", second.records);
        afterRepeat.call(this, second);
        return Promise.resolve(second);   
    }
    // 校验密码
    async check(password) {
        await this.cancel();
        // 拿到 checked 方法
        let checked = this.options.check.checked;
        // 输入密码
        let res = await this.record();
        // 如果报错并且错误等于 ERR_USER_CANCELED 就return
        if(res.err && res.err === Locker.ERR_USER_CANCELED){
            return Promise.resolve(res);
        }
        // 如果没有错误,密码不一致提示密码匹配不正确
        if(!res.err && password !== res.records){
            res.err = Locker.ERR_PASSWORD_MISMATCH
        }
        // 执行 checked 回调函数,返回结果出去
        checked.call(this, res);
        console.log("输入的密码:", res.records, "需要校验的密码:", password)
        // 再次执行check,失败了可以再次输入
        this.check(password);
        return Promise.resolve(res);
    }
}


9、添加 recorder.html 示例

添加文件 example/recorder.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>kaimo handlock recorder demo</title>
    <link rel="stylesheet" href="https://cdn.bootcdn.net/ajax/libs/element-ui/2.15.9/theme-chalk/index.css">
    <style>
        * { 
            padding: 0;
            margin: 0; 
        }
        html, body { 
            width: 100%; 
            height: 100%;
            overflow: hidden;
        }
        #container {
            position: relative;
            overflow: hidden;
            width: 100%;
            padding-top: 100%;
            height: 0px;
            background-color: white;
        }
        .control {
            text-align: center;
        }
    </style>
</head>
<body>
    <div id="container"></div>
    <div class="control">
        <button id="cancelBtn" class="el-button el-button--mini el-button--danger">取消</button>
    </div>
    <script src="../lib/kaimo-handlock-umd.js"></script>
    <script>
        // KaimoHandlock是打包暴露出来的,创建一个Recorder实例
        var recorder = new KaimoHandlock.Recorder({
            container: document.querySelector('#container'),
        });
        console.log(recorder)
        function recorded(res) {
            if(res.err){
                console.error(res.err)
                recorder.clearPath();
                if(res.err !== KaimoHandlock.Recorder.ERR_USER_CANCELED){
                    recorder.record().then(recorded);
                }
            }else{
                console.log("密码字符串:", res.records)
                recorder.record().then(recorded);
            }  
        }
        recorder.record().then(recorded);
        // 点击取消
        cancelBtn.onclick = function(){
            recorder.cancel();
            recorder.clearPath();
        }
    </script>
</body>
</html>



10、添加 locker.html 示例

添加文件 example/locker.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>kaimo handlock locker demo</title>
    <link rel="stylesheet" href="https://cdn.bootcdn.net/ajax/libs/element-ui/2.15.9/theme-chalk/index.css">
    <style>
        * { 
            padding: 0;
            margin: 0; 
        }
        html, body { 
            width: 100%; 
            height: 100%;
            overflow: hidden;
        }
        #container {
            position: relative;
            overflow: hidden;
            width: 100%;
            padding-top: 100%;
            height: 0px;
            background-color: white;
        }
        .control {
            text-align: center;
        }
    </style>
</head>
<body>
    <div id="container"></div>
    <div class="control">
        <button id="setPasswordBtn" class="el-button el-button--mini">设置密码</button>
        <button id="checkPasswordBtn" class="el-button el-button--mini">验证密码</button>
    </div>
    <script src="../lib/kaimo-handlock-umd.js"></script>
    <script>
        var password = localStorage.getItem('kaimo_handlock_passwd') || '1221322322';
        // KaimoHandlock是打包暴露出来的,创建一个Locker实例
        var locker = new KaimoHandlock.Locker({
            container: document.querySelector('#container'),
            check: {
                checked: function(res){
                    locker.clearPath();
                    console.log("checked--->", res)
                    if(res.err){
                        if(res.err === KaimoHandlock.Locker.ERR_PASSWORD_MISMATCH){
                            console.error("密码错误,请重新绘制!")
                        }else{
                            console.error(res.err)
                        }
                    }else{
                        console.log("密码正确!")
                    }
                },
            },
            update:{
                beforeRepeat: function(res){
                    locker.clearPath();
                    if(res.err){
                        console.log(`请连接至少${locker.options.minPoints}个点`)
                    }else{
                        console.log("请再次绘制相同图案")
                    }
                },
                afterRepeat: function(res){
                    locker.clearPath();
                    if(res.err){
                        if(res.err === KaimoHandlock.Locker.ERR_PASSWORD_MISMATCH){
                            console.log("两次绘制的图形不一致,请重新绘制!")
                        }else{
                            console.log(`请连接至少${locker.options.minPoints}个点`)
                        }
                    }else{
                        password = res.records;
                        localStorage.setItem('kaimo_handlock_passwd', password);
                        console.log("密码更新成功:", password)
                    }
                },
            }
        });
        console.log(locker)
        // 点击设置密码
        setPasswordBtn.onclick = function(){
            setPasswordBtn.classList.add("el-button--success");
            checkPasswordBtn.classList.remove("el-button--success");
            locker.clearPath();
            locker.update();
        }
        // 点击验证密码
        checkPasswordBtn.onclick = function(){
            checkPasswordBtn.classList.add("el-button--success");
            setPasswordBtn.classList.remove("el-button--success");
            locker.clearPath();
            locker.check(password);
        }
    </script>
</body>
</html>


11、启动服务测试

  • 第一个服务是 rollup 的
  • 第二个服务是 liveserve 的用于访问 html 页面


recorder.html 测试结果,需要切换到移动端模式,刷新之后就可以测试了。主要就是绘制完看效果,


48104c0b133946309dbc078acbae7990.png


locker.html 测试结果,相对上面一个,测试复杂一点,点击下面两个按钮是用于切换当前的模式的,切到设置模式会有两次的输入,如果两次都输入正确,就会把密码保存到localStorage,验证密码就是通过设置的密码去校验输入的密码是否一样。感兴趣的可以自己测试玩玩。


60260b327d134981bf9cd49f06c91d15.png


localStorage 里的缓存如下:

9acf9f2bffca4e8eb52581ab2f2ef9b2.png



12、打包之后的 umd 文件

我们看一下通过打包之后的 umd 文件,lib/kaimo-handlock-umd.js

!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self).KaimoHandlock={})}(this,(function(t){"use strict";function e(){
/*! regenerator-runtime -- Copyright (c) 2014-present, Facebook, Inc. -- license (MIT): https://github.com/facebook/regenerator/blob/main/LICENSE */
e=function(){return t};var t={},r=Object.prototype,n=r.hasOwnProperty,o="function"==typeof Symbol?Symbol:{},i=o.iterator||"@@iterator",a=o.asyncIterator||"@@asyncIterator",c=o.toStringTag||"@@toStringTag";function u(t,e,r){return Object.defineProperty(t,e,{value:r,enumerable:!0,configurable:!0,writable:!0}),t[e]}try{u({},"")}catch(t){u=function(t,e,r){return t[e]=r}}function s(t,e,r,n){var o=e&&e.prototype instanceof f?e:f,i=Object.create(o.prototype),a=new C(n||[]);return i._invoke=function(t,e,r){var n="suspendedStart";return function(o,i){if("executing"===n)throw new Error("Generator is already running");if("completed"===n){if("throw"===o)throw i;return _()}for(r.method=o,r.arg=i;;){var a=r.delegate;if(a){var c=E(a,r);if(c){if(c===h)continue;return c}}if("next"===r.method)r.sent=r._sent=r.arg;else if("throw"===r.method){if("suspendedStart"===n)throw n="completed",r.arg;r.dispatchException(r.arg)}else"return"===r.method&&r.abrupt("return",r.arg);n="executing";var u=l(t,e,r);if("normal"===u.type){if(n=r.done?"completed":"suspendedYield",u.arg===h)continue;return{value:u.arg,done:r.done}}"throw"===u.type&&(n="completed",r.method="throw",r.arg=u.arg)}}}(t,r,a),i}function l(t,e,r){try{return{type:"normal",arg:t.call(e,r)}}catch(t){return{type:"throw",arg:t}}}t.wrap=s;var h={};function f(){}function p(){}function d(){}var v={};u(v,i,(function(){return this}));var y=Object.getPrototypeOf,g=y&&y(y(k([])));g&&g!==r&&n.call(g,i)&&(v=g);var m=d.prototype=f.prototype=Object.create(v);function b(t){["next","throw","return"].forEach((function(e){u(t,e,(function(t){return this._invoke(e,t)}))}))}function w(t,e){function r(o,i,a,c){var u=l(t[o],t,i);if("throw"!==u.type){var s=u.arg,h=s.value;return h&&"object"==typeof h&&n.call(h,"__await")?e.resolve(h.__await).then((function(t){r("next",t,a,c)}),(function(t){r("throw",t,a,c)})):e.resolve(h).then((function(t){s.value=t,a(s)}),(function(t){return r("throw",t,a,c)}))}c(u.arg)}var o;this._invoke=function(t,n){function i(){return new e((function(e,o){r(t,n,e,o)}))}return o=o?o.then(i,i):i()}}function E(t,e){var r=t.iterator[e.method];if(void 0===r){if(e.delegate=null,"throw"===e.method){if(t.iterator.return&&(e.method="return",e.arg=void 0,E(t,e),"throw"===e.method))return h;e.method="throw",e.arg=new TypeError("The iterator does not provide a 'throw' method")}return h}var n=l(r,t.iterator,e.arg);if("throw"===n.type)return e.method="throw",e.arg=n.arg,e.delegate=null,h;var o=n.arg;return o?o.done?(e[t.resultName]=o.value,e.next=t.nextLoc,"return"!==e.method&&(e.method="next",e.arg=void 0),e.delegate=null,h):o:(e.method="throw",e.arg=new TypeError("iterator result is not an object"),e.delegate=null,h)}function x(t){var e={tryLoc:t[0]};1 in t&&(e.catchLoc=t[1]),2 in t&&(e.finallyLoc=t[2],e.afterLoc=t[3]),this.tryEntries.push(e)}function R(t){var e=t.completion||{};e.type="normal",delete e.arg,t.completion=e}function C(t){this.tryEntries=[{tryLoc:"root"}],t.forEach(x,this),this.reset(!0)}function k(t){if(t){var e=t[i];if(e)return e.call(t);if("function"==typeof t.next)return t;if(!isNaN(t.length)){var r=-1,o=function e(){for(;++r<t.length;)if(n.call(t,r))return e.value=t[r],e.done=!1,e;return e.value=void 0,e.done=!0,e};return o.next=o}}return{next:_}}function _(){return{value:void 0,done:!0}}return p.prototype=d,u(m,"constructor",d),u(d,"constructor",p),p.displayName=u(d,c,"GeneratorFunction"),t.isGeneratorFunction=function(t){var e="function"==typeof t&&t.constructor;return!!e&&(e===p||"GeneratorFunction"===(e.displayName||e.name))},t.mark=function(t){return Object.setPrototypeOf?Object.setPrototypeOf(t,d):(t.__proto__=d,u(t,c,"GeneratorFunction")),t.prototype=Object.create(m),t},t.awrap=function(t){return{__await:t}},b(w.prototype),u(w.prototype,a,(function(){return this})),t.AsyncIterator=w,t.async=function(e,r,n,o,i){void 0===i&&(i=Promise);var a=new w(s(e,r,n,o),i);return t.isGeneratorFunction(r)?a:a.next().then((function(t){return t.done?t.value:a.next()}))},b(m),u(m,c,"Generator"),u(m,i,(function(){return this})),u(m,"toString",(function(){return"[object Generator]"})),t.keys=function(t){var e=[];for(var r in t)e.push(r);return e.reverse(),function r(){for(;e.length;){var n=e.pop();if(n in t)return r.value=n,r.done=!1,r}return r.done=!0,r}},t.values=k,C.prototype={constructor:C,reset:function(t){if(this.prev=0,this.next=0,this.sent=this._sent=void 0,this.done=!1,this.delegate=null,this.method="next",this.arg=void 0,this.tryEntries.forEach(R),!t)for(var e in this)"t"===e.charAt(0)&&n.call(this,e)&&!isNaN(+e.slice(1))&&(this[e]=void 0)},stop:function(){this.done=!0;var t=this.tryEntries[0].completion;if("throw"===t.type)throw t.arg;return this.rval},dispatchException:function(t){if(this.done)throw t;var e=this;function r(r,n){return a.type="throw",a.arg=t,e.next=r,n&&(e.method="next",e.arg=void 0),!!n}for(var o=this.tryEntries.length-1;o>=0;--o){var i=this.tryEntries[o],a=i.completion;if("root"===i.tryLoc)return r("end");if(i.tryLoc<=this.prev){var c=n.call(i,"catchLoc"),u=n.call(i,"finallyLoc");if(c&&u){if(this.prev<i.catchLoc)return r(i.catchLoc,!0);if(this.prev<i.finallyLoc)return r(i.finallyLoc)}else if(c){if(this.prev<i.catchLoc)return r(i.catchLoc,!0)}else{if(!u)throw new Error("try statement without catch or finally");if(this.prev<i.finallyLoc)return r(i.finallyLoc)}}}},abrupt:function(t,e){for(var r=this.tryEntries.length-1;r>=0;--r){var o=this.tryEntries[r];if(o.tryLoc<=this.prev&&n.call(o,"finallyLoc")&&this.prev<o.finallyLoc){var i=o;break}}i&&("break"===t||"continue"===t)&&i.tryLoc<=e&&e<=i.finallyLoc&&(i=null);var a=i?i.completion:{};return a.type=t,a.arg=e,i?(this.method="next",this.next=i.finallyLoc,h):this.complete(a)},complete:function(t,e){if("throw"===t.type)throw t.arg;return"break"===t.type||"continue"===t.type?this.next=t.arg:"return"===t.type?(this.rval=this.arg=t.arg,this.method="return",this.next="end"):"normal"===t.type&&e&&(this.next=e),h},finish:function(t){for(var e=this.tryEntries.length-1;e>=0;--e){var r=this.tryEntries[e];if(r.finallyLoc===t)return this.complete(r.completion,r.afterLoc),R(r),h}},catch:function(t){for(var e=this.tryEntries.length-1;e>=0;--e){var r=this.tryEntries[e];if(r.tryLoc===t){var n=r.completion;if("throw"===n.type){var o=n.arg;R(r)}return o}}throw new Error("illegal catch attempt")},delegateYield:function(t,e,r){return this.delegate={iterator:k(t),resultName:e,nextLoc:r},"next"===this.method&&(this.arg=void 0),h}},t}function r(t,e,r,n,o,i,a){try{var c=t[i](a),u=c.value}catch(t){return void r(t)}c.done?e(u):Promise.resolve(u).then(n,o)}function n(t){return function(){var e=this,n=arguments;return new Promise((function(o,i){var a=t.apply(e,n);function c(t){r(a,o,i,c,u,"next",t)}function u(t){r(a,o,i,c,u,"throw",t)}c(void 0)}))}}function o(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}function i(t,e){for(var r=0;r<e.length;r++){var n=e[r];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(t,n.key,n)}}function a(t,e,r){return e&&i(t.prototype,e),r&&i(t,r),Object.defineProperty(t,"prototype",{writable:!1}),t}function c(t){return c=Object.setPrototypeOf?Object.getPrototypeOf.bind():function(t){return t.__proto__||Object.getPrototypeOf(t)},c(t)}function u(t,e){return u=Object.setPrototypeOf?Object.setPrototypeOf.bind():function(t,e){return t.__proto__=e,t},u(t,e)}function s(t,e){if(e&&("object"==typeof e||"function"==typeof e))return e;if(void 0!==e)throw new TypeError("Derived constructors may only return object or undefined");return function(t){if(void 0===t)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return t}(t)}function l(t){var e=function(){if("undefined"==typeof Reflect||!Reflect.construct)return!1;if(Reflect.construct.sham)return!1;if("function"==typeof Proxy)return!0;try{return Boolean.prototype.valueOf.call(Reflect.construct(Boolean,[],(function(){}))),!0}catch(t){return!1}}();return function(){var r,n=c(t);if(e){var o=c(this).constructor;r=Reflect.construct(n,arguments,o)}else r=n.apply(this,arguments);return s(this,r)}}var h={container:null,autoRender:!0,dotNum:4,defaultCircleColor:"#ddd",focusColor:"#33a06f",bgColor:"#fff",innerRadius:16,outerRadius:42,touchRadius:64,minPoints:4},f={beforeRepeat:function(){},afterRepeat:function(){}},p={checked:function(){}};function d(t,e){var r=e.x-t.x,n=e.y-t.y;return Math.sqrt(r*r+n*n)}function v(t,e,r,n,o){t.fillStyle=e,t.beginPath(),t.arc(r,n,o,0,2*Math.PI,!0),t.closePath(),t.fill()}function y(t,e,r,n,o){t.strokeStyle=e,t.beginPath(),t.arc(r,n,o,0,2*Math.PI,!0),t.closePath(),t.stroke()}function g(t,e,r,n,o,i){t.strokeStyle=e,t.beginPath(),t.moveTo(r,n),t.lineTo(o,i),t.stroke(),t.closePath()}var m=function(){function t(e){o(this,t),this.options=Object.assign({},h,e),this.container=null,this.circleCanvas=null,this.lineCanvas=null,this.moveCanvas=null,this.circles=[],this.recordingTask=null,this.options.autoRender&&this.render()}var r,i;return a(t,[{key:"render",value:function(){this.container=this.options.container||document.createElement("div");var t=container.getBoundingClientRect(),e=t.width,r=t.height;this.circleCanvas=document.createElement("canvas"),this.circleCanvas.width=this.circleCanvas.height=2*Math.min(e,r),Object.assign(this.circleCanvas.style,{position:"absolute",top:"50%",left:"50%",transform:"translate(-50%, -50%) scale(0.5)"}),this.lineCanvas=this.circleCanvas.cloneNode(!0),this.moveCanvas=this.circleCanvas.cloneNode(!0),container.appendChild(this.lineCanvas),container.appendChild(this.moveCanvas),container.appendChild(this.circleCanvas),this.container.addEventListener("touchmove",(function(t){return t.preventDefault()}),{passive:!1}),this.clearPath()}},{key:"clearPath",value:function(){this.circleCanvas||this.render();var t=this.circleCanvas,e=this.lineCanvas,r=this.moveCanvas,n=this.options,o=t.getContext("2d"),i=e.getContext("2d"),a=r.getContext("2d"),c=t.width,u=n.dotNum,s=n.defaultCircleColor,l=n.innerRadius;o.clearRect(0,0,c,c),i.clearRect(0,0,c,c),a.clearRect(0,0,c,c);for(var h=Math.round(c/(u+1)),f=[],p=1;p<=u;p++)for(var d=1;d<=u;d++){var y=h*p,g=h*d;v(o,s,g,y,l);var m={x:g,y:y};m.pos=[p,d],f.push(m)}this.circles=f}},{key:"record",value:(i=n(e().mark((function r(){var n,o,i,a,c,u,s,l,h,f,p,m=this;return e().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return n=this.circleCanvas,o=this.lineCanvas,i=this.moveCanvas,a=this.options,c=n.getContext("2d"),u=o.getContext("2d"),s=i.getContext("2d"),l=[],h=function(t){"touchstart"===t.type&&(l=[],m.clearPath());for(var e,r,n,o=a.bgColor,h=a.focusColor,f=a.innerRadius,p=a.outerRadius,b=a.touchRadius,w=t.changedTouches[0],E=w.clientX,x=w.clientY,R=(e=E,r=x,n=i.getBoundingClientRect(),{x:2*(e-n.left),y:2*(r-n.top)}),C=0;C<m.circles.length;C++){var k=m.circles[C],_=k.x,L=k.y;if(d(k,R)<b){if(v(c,o,_,L,p),v(c,h,_,L,f),y(c,h,_,L,p),l.length){var P=l[l.length-1],O=P.x,j=P.y;g(u,h,_,L,O,j)}var S=m.circles.splice(C,1);l.push(S[0]);break}}if(l.length){var T=l[l.length-1],N=T.x,A=T.y,M=R.x,D=R.y;s.clearRect(0,0,i.width,i.height),g(s,h,N,A,M,D)}},n.addEventListener("touchstart",h),n.addEventListener("touchmove",h),f={},p=new Promise((function(e,r){f.cancel=function(){var r=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},i=m.recordingTask.promise;return r.err=r.err||t.ERR_USER_CANCELED,n.removeEventListener("touchstart",h),n.removeEventListener("touchmove",h),document.removeEventListener("touchend",o),e(r),m.recordingTask=null,i};var o=function t(r){if(s.clearRect(0,0,i.width,i.height),l.length){n.removeEventListener("touchstart",h),n.removeEventListener("touchmove",h),document.removeEventListener("touchend",t);var o=null;l.length<a.minPoints&&(o="连接点数至少需要".concat(a.minPoints,"个")),e({err:o,records:l.map((function(t){return t.pos.join("")})).join("")}),m.recordingTask=null}};document.addEventListener("touchend",o)})),f.promise=p,this.recordingTask=f,e.abrupt("return",p);case 10:case"end":return e.stop()}}),r,this)}))),function(){return i.apply(this,arguments)})},{key:"cancel",value:(r=n(e().mark((function r(){return e().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:if(!this.recordingTask){e.next=2;break}return e.abrupt("return",this.recordingTask.cancel());case 2:return e.abrupt("return",Promise.resolve({err:t.ERR_NO_TASK}));case 3:case"end":return e.stop()}}),r,this)}))),function(){return r.apply(this,arguments)})}],[{key:"ERR_USER_CANCELED",get:function(){return"用户已经取消"}},{key:"ERR_NO_TASK",get:function(){return"暂无任务可执行"}}]),t}(),b=function(t){!function(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Super expression must either be null or a function");t.prototype=Object.create(e&&e.prototype,{constructor:{value:t,writable:!0,configurable:!0}}),Object.defineProperty(t,"prototype",{writable:!1}),e&&u(t,e)}(s,t);var r,i,c=l(s);function s(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};return o(this,s),t.update=Object.assign({},f,t.update),t.check=Object.assign({},p,t.check),c.call(this,t)}return a(s,[{key:"update",value:(i=n(e().mark((function t(){var r,n,o,i;return e().wrap((function(t){for(;;)switch(t.prev=t.next){case 0:return t.next=2,this.cancel();case 2:return r=this.options.update.beforeRepeat,n=this.options.update.afterRepeat,t.next=5,this.record();case 5:if(!(o=t.sent).err||o.err!==s.ERR_USER_CANCELED){t.next=8;break}return t.abrupt("return",Promise.resolve(o));case 8:if(!o.err){t.next=12;break}return this.update(),r.call(this,o),t.abrupt("return",Promise.resolve(o));case 12:return console.log("第一次密码:",o.records),r.call(this,o),t.next=16,this.record();case 16:if(!(i=t.sent).err||i.err!==s.ERR_USER_CANCELED){t.next=19;break}return t.abrupt("return",Promise.resolve(i));case 19:return i.err||o.records===i.records||(i.err=s.ERR_PASSWORD_MISMATCH),this.update(),console.log("第二次密码:",i.records),n.call(this,i),t.abrupt("return",Promise.resolve(i));case 24:case"end":return t.stop()}}),t,this)}))),function(){return i.apply(this,arguments)})},{key:"check",value:(r=n(e().mark((function t(r){var n,o;return e().wrap((function(t){for(;;)switch(t.prev=t.next){case 0:return t.next=2,this.cancel();case 2:return n=this.options.check.checked,t.next=5,this.record();case 5:if(!(o=t.sent).err||o.err!==s.ERR_USER_CANCELED){t.next=8;break}return t.abrupt("return",Promise.resolve(o));case 8:return o.err||r===o.records||(o.err=s.ERR_PASSWORD_MISMATCH),n.call(this,o),console.log("输入的密码:",o.records,"需要校验的密码:",r),this.check(r),t.abrupt("return",Promise.resolve(o));case 13:case"end":return t.stop()}}),t,this)}))),function(t){return r.apply(this,arguments)})}],[{key:"ERR_PASSWORD_MISMATCH",get:function(){return"密码不匹配"}}]),s}(m);t.Locker=b,t.Recorder=m,Object.defineProperty(t,"__esModule",{value:!0})}));



13、目录结构

目录结构如下:

d714f396c4064d8f9a686c14756f28a7.png

目录
相关文章
|
1天前
|
前端开发 JavaScript
JS长按保存canvas绘图
JS长按保存canvas绘图
6 0
|
1天前
|
JavaScript 前端开发
JavaScript实现识别二维码信息功能
JavaScript实现识别二维码信息功能
7 1
|
1天前
|
JavaScript 计算机视觉
原生js通过年龄判断是否可以抽奖
原生js通过年龄判断是否可以抽奖
8 0
|
1天前
|
JavaScript
js实现全屏功能——易懂版
js实现全屏功能——易懂版
5 0
|
2天前
|
存储 JavaScript 前端开发
原生JS如何实现验证码
原生JS如何实现验证码
9 0
|
2天前
|
JavaScript 安全 前端开发
原生JS实现一键复制,一键粘贴
原生JS实现一键复制,一键粘贴
10 0
|
9天前
|
JavaScript 前端开发 API
Vue.js 中子组件向父组件传值的方法
Vue.js 中子组件向父组件传值的方法
23 2
|
10天前
|
JavaScript 前端开发 API
如何利用JavaScript和Electron构建具有丰富功能的桌面应用
【4月更文挑战第30天】如何利用JavaScript和Electron构建具有丰富功能的桌面应用
5 0
|
11天前
|
JavaScript 前端开发
实现一个JavaScript动态日期功能
实现一个JavaScript动态日期功能
|
11天前
|
JavaScript 前端开发
JavaScript模糊搜索功能
JavaScript模糊搜索功能