前言
我的截图插件js-web-screen-shot,在三年的时间里,经历了从1.0.0到1.9.9的版本迭代。随着功能的不断增加,原本的入口文件变得越来越复杂和混乱,代码行数已接近1500行。
最近,在着手开发2.0大版本的功能,面对这些复杂的代码,我感到非常困扰,这也使得很多想要为项目贡献新功能的人因为代码的复杂性而望而却步。
经过综合考量后,我决定优化和拆分入口文件中的冗余部分,使结构更加简洁明了。本文就跟大家分享下我的优化过程,欢迎各位感兴趣的开发者阅读本文。
逻辑梳理
从入参开始,逐行分析代码,捋清函数间的依赖关系,这是我们首先要做的。我在做复杂的事情时,习惯把自己脑子里想的东西以思维导图的形式呈现出来,如下图所示,插件从实例化到加载,总共分为9个步骤:
- 获取用户配置
- 创建辅助DOM(webrtc模式时需要用到)
- 实例化全局响应式对象
- 提取可选配置
- 获取截图区域的canvas容器
- 修改容器的可滚动状态
- 加载截图组件
- 调整容器层级
- 创建事件监听
这9个步骤中,加载截图组件是其核心处理逻辑,也是依赖关系最错综复杂的地方。此处就不做过多赘述了,感兴趣的开发者可以结合图中的路线去翻阅main.ts
文件中的**load方法**。
制定方案
分析完load方法,以及与其关联的类内部的私有方法。它们都有1个共同点:
- 在截图期间对类内部引用类型和基本类型数据的各种计算与修改
那么,我们能做的就是把这些计算逻辑拆分成方法,独立出去,只关注输入于输出,这样就大大降低了代码的复杂度,使其更易维护。
代码拆分
我新建了两个ts文件,用来存放拆分出来的方法。
- LoadCoreComponents.ts 处理组件中的数据计算处理方法
- mouseDownCore.ts 处理鼠标的按下、移动、抬起事件
考虑到load方法所依赖的方法较多,在ts文件里用function
去声明的话,后续维护查找时不够直观。因此,我采用了const
+export
的方式。
组件方法拆分
在LoadCoreComponents.ts
文件中,我拆了19个方法出来。在本章节中,我将挑几个具有代表性的方法来做讲解。
操作裁剪框
在操作裁剪框的时候,方法内部需要修改类内部基本类型的数据,我们都知道:在js里,当函数的参数类型是基本类型的时候,通过值传递。那么,拆分出来后,如何来更新这部分数据呢?
聪明的开发者应该已经想到了。没错,那就是通过回调函数来实现更新,代码如下所示:
- 函数的入参接受一个回调函数,返回值为genericMethodPostbackType类型,定义了三个属性:
- code
- msg
- data
- 在函数内部定义res,经过一系列的计算后,修改res对象里的值,在恰当的时机去执行回调函数来更新数据
const operatingCutOutBox = (
currentX: number,
currentY: number,
startX: number,
startY: number,
width: number,
height: number,
context: CanvasRenderingContext2D,
data: InitData,
dpr: number,
containerInfo: {
screenShotContainer: HTMLCanvasElement | null | undefined;
screenShotImageController: HTMLCanvasElement;
},
containerVariable: {
movePosition: movePositionType;
cutOutBoxBorderArr: Array<cutOutBoxBorder>;
borderOption: number | null;
},
callerCallback: (res: genericMethodPostbackType) => void
) => {
const res: genericMethodPostbackType = { code: 0, msg: "", data: null };
// canvas元素不存在
if (containerInfo.screenShotContainer == null) {
return;
}
// 获取鼠标按下时的坐标
const { moveStartX, moveStartY } = containerVariable.movePosition;
// 裁剪框边框节点事件存在且裁剪框未进行操作,则对鼠标样式进行修改
if (
containerVariable.cutOutBoxBorderArr.length > 0 &&
!data.getDraggingTrim()
) {
//...其他代码省略,这里会经过一系列的计算,修改res对象的值,最后调用callerCallback方法来更新数据...//
callerCallback(res)
};
然后,我们来看下调用时的代码,传入了croppingBoxCallerCallback
函数,在函数内部,根据code
来更新类内部所依赖的的数据。
// 执行裁剪框操作函数
operatingCutOutBox(
currentX,
currentY,
startX,
startY,
width,
height,
this.screenShotCanvas,
this.data,
this.dpr,
{
screenShotContainer: this.screenShotContainer,
screenShotImageController: this.screenShotImageController
},
{
movePosition: this.movePosition,
cutOutBoxBorderArr: this.cutOutBoxBorderArr,
borderOption: this.borderOption
},
this.croppingBoxCallerCallback
);
// 裁剪框回调
// 对组件内部所依赖的数据做处理
private croppingBoxCallerCallback = (res: genericMethodPostbackType) => {
const { code, data } = res;
if (code === 1 && typeof data === "number") {
this.borderOption = data;
}
if (code === 2 && typeof data === "boolean") {
this.mouseInsideCropBox = data;
}
if (code === 3 && typeof data === null) {
this.borderOption = null;
}
if ((code === 4 || code === 5) && typeof data != null) {
this.tempGraphPosition = res.data as drawCutOutBoxReturnType;
}
};
注意:此处只列举了关键代码,完整代码请移步:
处理涂鸦绘制
在画布上进行涂鸦绘制时,会更新类内部的 drawStatus
变量,我们拆分出来后,也是用同样的办法去更新,除了更新类内部的变量外,我们还用到了类内部的方法showLastHistory
,我们只需要把它当作参数传入,在需要的时候调用即可,如下所示:
const handleGraffitiDraw = (
drawStatus: boolean,
startX: number,
startY: number,
tempWidth: number,
tempHeight: number,
currentX: number,
currentY: number,
degreeOfBlur: number,
data: InitData,
useRatioArrow: boolean,
containerInfo: {
screenShotCanvas: CanvasRenderingContext2D;
},
containerFn: {
showLastHistory: () => void;
},
callerCallback: (res: genericMethodPostbackType) => void
) => {
const res: genericMethodPostbackType = {
code: 0,
data: null,
msg: ""
};
switch (data.getToolName()) {
case "square":
drawRectangle(
startX,
startY,
tempWidth,
tempHeight,
data.getSelectedColor(),
data.getPenSize(),
containerInfo.screenShotCanvas
);
break;
case "round":
drawCircle(
containerInfo.screenShotCanvas,
currentX,
currentY,
startX,
startY,
data.getPenSize(),
data.getSelectedColor()
);
break;
case "right-top":
// 绘制等比例箭头
if (useRatioArrow) {
drawLineArrow(
containerInfo.screenShotCanvas,
startX,
startY,
currentX,
currentY,
30,
10,
data.getPenSize(),
data.getSelectedColor()
);
break;
}
// 绘制递增变粗箭头
new DrawArrow().draw(
containerInfo.screenShotCanvas,
startX,
startY,
currentX,
currentY,
data.getSelectedColor(),
data.getPenSize()
);
break;
case "brush":
// 画笔绘制
drawPencil(
containerInfo.screenShotCanvas,
currentX,
currentY,
data.getPenSize(),
data.getSelectedColor()
);
break;
case "mosaicPen":
// 当前为马赛克工具则修改绘制状态
// 前面做了判断,此处需要特殊处理
if (!drawStatus) {
containerFn.showLastHistory();
// 返回一个特殊值,用于修改调用组件的内部状态
res.code = 1;
res.data = true;
res.msg = "需要更新组件状态";
callerCallback(res);
}
// 绘制马赛克,为了确保鼠标位置在绘制区域中间,所以对x、y坐标进行-10处理
drawMosaic(
currentX - 10,
currentY - 10,
data.getMosaicPenSize(),
degreeOfBlur,
containerInfo.screenShotCanvas
);
break;
default:
break;
}
return res;
};
在调用的时候,因为这个回调函数不会在类内部的其他地方复用,因此我们只需要函数内部多声明一个函数即可。
const callerCallback = (res: genericMethodPostbackType) => {
if (
res.code === 1 &&
res.data != null &&
typeof res.data === "boolean"
) {
this.drawStatus = res.data;
}
};
// 处理涂鸦绘制
handleGraffitiDraw(
this.drawStatus,
startX,
startY,
tempWidth,
tempHeight,
currentX,
currentY,
this.degreeOfBlur,
this.data,
this.plugInParameters.getRatioArrow(),
{
screenShotCanvas: this.screenShotCanvas
},
{ showLastHistory: this.showLastHistory },
callerCallback
);
鼠标事件拆分
在类内部处理鼠标事件时,代码也比较冗余,有很多逻辑可以拆出去,为了便于维护,我创建了独立的文件mouseDownCore.ts
来放这些拆出来的方法,因为拆分思路与组件方法的拆分思路是一致的,本章节就不做过多的代码讲解了。
在鼠标事件的处理中,有很多地方涉及到引用类型的数据修改(直接赋值,如下图所示),如果直接在拆分出来的函数内部去改的话,类内部的变量并不会得到更新,因为引用地址发生了改变,那么有没有什么更好的办法呢?
相信很多开发者已经想到了,那就是用Object.assign
,这样就可以在不改变引用地址的情况下去更新对象内部的值。
// 保存边框节点信息
Object.assign(
containerVariable.cutOutBoxBorderArr,
saveBorderArrInfo(data.getBorderSize(), containerVariable.drawGraphPosition)
);
完整代码请移步:
优化后的效果
代码经过拆分与优化后,入口文件的代码行数从1459优化到了843。
项目地址
本文所列举的代码,其对应的项目请移步:
最近在看新的工作机会,如果有公司招聘前端/全栈(偏前)开发岗位的话,可以联系我。
- 我的邮箱:magicalprogrammer@qq.com
- 我的微信:Baymax-kt
写在最后
至此,文章就分享完毕了。
我是神奇的程序员,一位前端开发工程师。
如果你对我感兴趣,请移步我的个人网站,进一步了解。
- 文中如有错误,欢迎在评论区指正,如果这篇文章帮到了你,欢迎点赞和关注😊
- 本文首发于神奇的程序员公众号,未经许可禁止转载💌
评论区