最近看了许式伟老师的架构课,其中实战部分是从一个画图程序开始讲起。之前看了大多数是理论层面的内容,突然一下子接触到代码感觉还是有点跳跃,本文先从v26分支上试着分析下代码中实现的功能。
代码见v26分支
服务端程序就一个main.go文件,引入了net相关的http,url,httputil几个包。首先程序先注册了一个/api/的请求路径,使用http.StripPrefix来执行这个路径的请求。StripPrefix会把request的URL前缀/api/去掉后请求后返回。这里的apiReverseProxy定义了一个反向代理为http://localhost:9999。调用http.HandleFunc将根目录/注册到调用handleDefault方法,其实等于是访问www/index.htm页面。
客户端渲染的页面就是www/index.htm页面。页面加载了如下几个js页面:
<script src="dom.js"></script>
<script src="view.js"></script>
<script src="creator/path.js?v=7"></script>
<script src="creator/freepath.js?v=7"></script>
<script src="creator/rect.js?v=7"></script>
<script src="accel/menu.js"></script>
首先看dom.js,可以看到主要是定义了描述线条样式的类QLineStyle,保存的是宽度width和颜色color。其他的类代表绘制的图形,包括QLine,QRect, QEllipse,QPath。
以QPath代码为例,主要定义了坐标(points)、是否结束(close)、线条类型(lineStyle)3个属性和一个绘图方法(onpaint):
class QPath {
constructor(points, close, lineStyle) {
this.points = points
this.close = close
this.lineStyle = lineStyle
}
onpaint(ctx) {
let n = this.points.length
if (n < 1) {
return
}
let points = this.points
let lineStyle = this.lineStyle
ctx.lineWidth = lineStyle.width
ctx.strokeStyle = lineStyle.color
ctx.beginPath()
ctx.moveTo(points[0].x, points[0].y)
for (let i = 1; i < n; i++) {
ctx.lineTo(points[i].x, points[i].y)
}
if (this.close) {
ctx.closePath()
}
ctx.stroke()
}
}
QPaintDoc类代表整个浏览器DOM树的根节点,除了有绘制方法(onpaint)外,还有一个addShape方法把图形添加到数组中:
class QPaintDoc {
constructor() {
this.shapes = []
}
addShape(shape) {
if (shape != null) {
this.shapes.push(shape)
}
}
onpaint(ctx) {
let shapes = this.shapes
for (let i in shapes) {
shapes[i].onpaint(ctx)
}
}
}
view.js定义了ViewModel层的核心功能,实现了QPaintView类。这个类在index.htm页面的id="drawing"区域定义了几种事件类型,分别是onmousedown、onmousemove、onmouseup、ondblclick和onkeydown。除了实现以上的事件类型,还实现了Controller的注册、调用和停用。
registerController(name, controller) {
if (name in this.controllers) {
alert("Controller exists: " + name)
} else {
this.controllers[name] = controller
}
}
invokeController(name) {
this.stopController()
if (name in this.controllers) {
let controller = this.controllers[name]
this._setCurrent(name, controller())
}
}
stopController() {
if (this._current != null) {
this._current.stop()
this._setCurrent("", null)
}
}
invalidate函数用来重新绘制绘图区域,当鼠标放到矩形绘图区域或者绘图区域发生变化就会全部清空后重新绘制图形。
function invalidate(reserved) {
qview.invalidateRect(null)
}
invalidateRect(reserved) {
let ctx = this.drawing.getContext("2d")
let bound = this.drawing.getBoundingClientRect()
ctx.clearRect(0, 0, bound.width, bound.height)
this.onpaint(ctx)
}
其余文件都是Controller功能的实现。
先看menue.js主要实现了图形菜单的创建和相关Controller的激活、调用,线条宽度改变,颜色改变以及辅助功能鼠标坐标的展示。
installControllers()
installPropSelectors()
installMousePos()
rect.js注册了LineCreator,RectCreator,EllipseCreator,CircleCreator这四个Controller并实现了这几类图形的创建功能:
buildShape() {
let rect = this.rect
let r = normalizeRect(rect)
switch (this.shapeType) {
case "line":
return new QLine(rect.p1, rect.p2, qview.lineStyle)
case "rect":
return new QRect(r, qview.lineStyle)
case "ellipse":
let rx = r.width / 2
let ry = r.height / 2
return new QEllipse(r.x + rx, r.y + ry, rx, ry, qview.lineStyle)
case "circle":
let rc = Math.sqrt(r.width * r.width + r.height * r.height)
return new QEllipse(rect.p1.x, rect.p1.y, rc, rc, qview.lineStyle)
default:
alert("unknown shapeType: " + this.shapeType)
return null
}
}
最后path.js和freepath.js实现的都是路径相关的功能,实现方式和其他图形基本类似。通过go build编译完成后,该程序就实现了简单的浏览器画图程序的功能。原来画图程序也可以这么简单的来实现。