使用 rollup 打包一个原生 js + canvas 实现的移动端手势解锁功能组件
Posted 凯小默的博客
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了使用 rollup 打包一个原生 js + canvas 实现的移动端手势解锁功能组件相关的知识,希望对你有一定的参考价值。
说明
原型和操作流程
用户用手指按顺序依次划过 9 个原点中的若干个(必须不少于 4 个点),如果划过的点的数量和顺序与之前用户设置的相同,那么当用户的手指离开屏幕时,判定为密码输入正确,否则密码错误。
要求:实现一个移动网页,允许用户设置手势密码和验证手势密码。已设置的密码记录在本地 localStorage 中。
stat 1:设置密码。
用户选择设置密码时,要提示用户输入手势密码。
stat 2:密码长度太短
如果用户输入的密码不足 5 个点,提示用户密码太短。
stat 3:再次输入密码
设置成功一次密码后,提示用户再次输入密码。
stat 4:两次密码输入不一致
如果用户输入的两次密码不一致,提示并重置,重新开始设置密码
stat 5:密码设置成功
如果两次输入一致,密码设置成功,更新 localStorage。
stat 6:验证密码 - 不正确
切换单选框进入验证密码模式,将用户输入的密码与保存的密码相比较,如果不一致,则提示输入密码不正确,重置为等待用户输入。
stat 7:验证密码 - 正确
如果用户输入的密码与 localStorage 中保存的密码一致,则提示密码正确。
组件设计步骤
组件设计一般来说包括 7 个步骤,分别是:理解需求、技术选型、结构(UI)设计、数据和 API 设计、流程设计、兼容性和细节优化,以及工具和工程化。
理解需求
需要由使用者决定设置密码的过程里执行什么操作、验证密码的过程和密码验证成功后执行什么操作,应当将过程节点开放出来,让使用者来决定。
技术选型
UI 展现的核心是九宫格和选中的小圆点,这里我们使用 canvas 去实现效果
- SVG 原生操作的 API 不是很方便,可以使用Snap.svg,但移动端兼容性不如 DOM 和 Canvas 好
- DOM 的优点是容易实现响应式,事件处理简单,布局也不复杂,但是要计算斜线的长度和斜率。
第一个细节:用 DOM 构造一个正方形的容器,使用 padding-top:100%
撑开容器高度使它等于容器宽度。
#container
position: relative;
overflow: hidden;
width: 100%;
padding-top: 100%;
height: 0px;
background-color: white;
第二个细节:为了在 retina 屏上获得清晰的显示效果,可以将 Canvas 的宽高增加一倍,然后通过 transform: scale(0.5)
来缩小到匹配容器宽高。
#container canvas
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%) scale(0.5);
Retina:一种新型高分辨率的显示标准,是把更多的像素点压缩至一块屏幕里,从而达到更高的分辨率并提高屏幕显示的细腻程度。由摩托罗拉公司研发。最初该技术是用于Moto Aura上。这种分辨率在正常观看距离下足以使人肉眼无法分辨其中的单独像素。也被称为
视网膜显示屏
。
原因:canvas 绘制的图形是位图,即栅格图像或点阵图像,当将它渲染到高清屏时,会被放大,每个像素点会用 devicePixelRatio 的平方个物理像素点来渲染,因此图片会变得模糊。
另外设置一下canvas的宽高相等
let width = 2 * container.getBoundingClientRect().width;
canvas.width = canvas.height = width;
这样就得到了一个正方形
<!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 canvas demo</title>
<style>#container
position: relative;
overflow: hidden;
width: 100%;
padding-top: 100%;
height: 0px;
background-color: white;
#container canvas
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%) scale(0.5);
</style>
</head>
<body>
<div id="container">
<canvas id="canvas"></canvas>
</div>
<script>let width = 2 * container.getBoundingClientRect().width;
canvas.width = canvas.height = width;</script>
</body>
</html>
结构设计
因为 Canvas 的渲染机制里,要更新画布的内容,需要刷新要更新的区域重新绘制。因此我们有必要把频繁变化的内容和基本不变的内容分层管理,这样能显著提升性能。
这里我们可以分成三层
- 底层:已经画好的线
- 中层:排列的九个点
- 上层:随着手指头移动的那个线段
下面确定圆点的位置:我们使用 4 x 4 的这种,需要遍历绘制实心圆,另外 Touch 相对于屏幕的坐标也需要转换为 Canvas 相对于画布的坐标。
API 设计
Recorder 只负责记录用户手势行为
- render:进行渲染
- record:负责记录
- clearPath:负责在画布上清除上一次记录的结果
- cancel:负责终止记录过程
对于输出结果,可以使用选中圆点的行列坐标拼接起来得到一个唯一的序列。
例如:11121323
就是如下选择图形:
让 Locker 继承 Recorder,管理实际的设置和验证密码的过程
- check:负责校验
- update:负责更新密码
另外我们可以将外观配置抽取出来:比如颜色,个数等等。
流程设计
验证密码的流程:
设置密码的流程:
打包组件
这里我们采用 rollup 进行组件的打包,rollup 它是一个类似于 webpack 的打包工具,区别于 webpack,它更适合一个库的打包。
核心概念
-
input
:入口文件,类比于 webpack 的 entry,它指明了我们库文件入口位置。 -
output
:输出位置,它指明了打包后的输出信息,包括:输出目录,打包文件名等。 -
plugins
:插件,rollup 在构建过程中,插件可提供一些辅助功能,例如:alias别名解析、转义ES6等。 -
external
:当我们的库依赖于其它第三方库时,我们不需要把这些第三方库一起打包,而是应该把依赖写在external里面。
构建说明
-
umd
:此选项构建出来的库文件是一个通用模式,可以通过不同的方式去使用:script 标签引入,ES Module 规范引入和 CommonJs 规范引入等。 -
cjs
:此选项构建出来的库文件主要为 CommonJs 规范,可在 Node 环境中使用。 -
es
:此版本构建出来的库文件主要为 ES Module 规范,可在支持 ES Module 也就是import/export
的环境中使用。
在 package.json
中配置打包命令:
"scripts":
"dev": "rollup -wc"
-
-c
:为--config
的缩写,表示设置 rollup 打包的配置。 -
-w
:为--watch
的缩写,在本地开发环境添加-w
参数可以监控源文件的变化,自动重新打包。
常用插件
代码实现
1、新建初始化项目
npm
2、安装依赖
npm
为了让库文件具有更好的兼容性,需要把ES6代码在打包的时候转义成ES5。
# 安装rollup插件包
npm install @rollup/plugin-babel -D
# 安装babel相关包
npm install
在根目录下新建 .babelrc
文件,并撰写如下内容:
"presets": [
"@babel/preset-env"
]
需要对生产环境进行压缩
# 安装代码压缩插件
npm install
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,)
let rect = canvas.getBoundingClientRect();
return
x: 2 * (x - rect.left),
y: 2 * (y - rect.top)
;
// 计算连点之间的距离
export function distance(p1,)
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,)
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,)
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,)
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,以上是关于使用 rollup 打包一个原生 js + canvas 实现的移动端手势解锁功能组件的主要内容,如果未能解决你的问题,请参考以下文章