我的截图插件 js-web-screen-shot ,在三年的时间里,经历了从 1.0.0 到 1.9.9 的版本迭代。随着功能的不断增加,原本的入口文件变得越来越复杂和混乱,代码行数已接近 1500 行。
最近,在着手开发 2.0 大版本的功能,面对这些复杂的代码,我感到非常困扰,这也使得很多想要为项目贡献新功能的人因为代码的复杂性而望而却步。
经过综合考量后,我决定优化和拆分入口文件中的冗余部分,使结构更加简洁明了。本文就跟大家分享下我的优化过程,欢迎各位感兴趣的开发者阅读本文。
从入参开始,逐行分析代码,捋清函数间的依赖关系,这是我们首先要做的。我在做复杂的事情时,习惯把自己脑子里想的东西以思维导图的形式呈现出来,如下图所示,插件从实例化到加载,总共分为 9 个步骤:
这 9 个步骤中,加载截图组件是其核心处理逻辑,也是依赖关系最错综复杂的地方。此处就不做过多赘述了,感兴趣的开发者可以结合图中的路线去翻阅mian.ts
文件中的**load 方法**。
分析完 load 方法,以及与其关联的类内部的私有方法。它们都有 1 个共同点:
那么,我们能做的就是把这些计算逻辑拆分成方法,独立出去,只关注输入于输出,这样就大大降低了代码的复杂度,使其更易维护。
我新建了两个 ts 文件,用来存放拆分出来的方法。
考虑到 load 方法所依赖的方法较多,在 ts 文件里用function
去声明的话,后续维护查找时不够直观。因此,我采用了const
+export
的方式。
在LoadCoreComponents.ts
文件中,我拆了 19 个方法出来。在本章节中,我将挑几个具有代表性的方法来做讲解。
在操作裁剪框的时候,方法内部需要修改类内部基本类型的数据,我们都知道:在 js 里,当函数的参数类型是基本类型的时候,通过值传递。那么,拆分出来后,如何来更新这部分数据呢?
聪明的开发者应该已经想到了。没错,那就是通过回调函数来实现更新,代码如下所示:
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
至此,文章就分享完毕了。
我是神奇的程序员,一位前端开发工程师。
如果你对我感兴趣,请移步我的个人网站,进一步了解。
这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。
V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。
V2EX is a community of developers, designers and creative people.