Heim > Artikel > Web-Frontend > Tetris für JavaScript – überarbeitet
Rekonstruieren Sie die Projektstruktur
Die Hauptprojektstruktur besteht darin, die ursprüngliche App in src umzubenennen, was bedeutet, dass das Skript und weniger Quellcode vorhanden sind. Natürlich wurde auch app/src, in dem ursprünglich der Skript-Quellcode gespeichert war, in src/scripts umbenannt.
[root> |-- index.html : 入口 |-- js/ : 构建生成的脚本 |-- css/ : 构建生成的样式表 |-- lib/ : bower 引入的库 `-- src/ : 前端源文件 |-- less : 样式表源文件 `-- scripts : 脚本(es6)源文件
Darüber hinaus sind die Basisskripte in Module unterteilt und während des Rekonstruktionsprozesses wurden zwei Unterverzeichnisse, model und tetris, erstellt.
Strukturanalyse
Vor dem Refactoring wurde eine einfache Strukturanalyse durchgeführt, bei der hauptsächlich mehrere Module aufgeteilt und im Modellverzeichnis abgelegt wurden. Während des Refactorings und Schreibens neuer Funktionen wurde das Tetris-Verzeichnis erstellt, in dem Funktionsklassen und Hilfsklassen platziert werden. Die wichtigste Funktion befindet sich jedoch immer noch in scrits/tetris.js.
Das Folgende ist das Bild, das ich gezeichnet habe, als ich das Modell zum ersten Mal analysiert habe:
Refactoring
Beim Schreiben eines Programms ist Refactoring immer dabei sehr notwendig, aber auch ein sehr fehleranfälliges Teil. Der gesamte Rekonstruktionsprozess von Tetris ist aus dem Commit-Protokoll des Arbeitszweigs im Quellcode ersichtlich.
Der wichtigste Punkt beim Refactoring ist: Ändern Sie die Codestruktur, aber nicht die Logik. Das heißt, jeder Schritt des Refactorings muss den Code auf der Grundlage der Sicherstellung der ursprünglichen Geschäftslogik ändern. Obwohl dies nicht zu 100 % erreichbar ist, müssen wir unser Bestes geben, um diesem Prinzip zu folgen, damit wir während des Refactorings nicht verwirrt werden Es tritt ein unerklärlicher Fehler auf. Dieser Punkt sollte im Buch „Refactoring to Improve the Design of Existing Code“ erwähnt werden.
Obwohl ich nicht sicher bin, ob das Prinzip, den Code zu ändern, ohne die Logik zu ändern, im Buch „Refactoring to Improve the Design of Existing Code“ erwähnt wird, empfehle ich dennoch jedem, dieses Buch zu lesen. Refactoring spielt eine sehr wichtige Rolle in der Entwicklung, aber der Refactoring-Prozess umfasst viele Entwurfsmuster, sodass auch Entwurfsmuster gelesen werden müssen.
Private Member
Während des Refactoring-Prozesses habe ich allen Klassen private Memberdefinitionen hinzugefügt. Der Zweck besteht darin, den versehentlichen Zugriff auf Mitglieder zu vermeiden, auf die bei der Verwendung nicht zugegriffen werden sollte (bezieht sich normalerweise auf versehentliches Überschreiben, aber manchmal kann auch das versehentliche Festlegen von Werten zu Fehlern führen).
Was das Thema private Mitglieder betrifft, habe ich es in ES5 Simulated ES6 Symbol Implementation of Private Members besprochen. Hier habe ich nicht die in diesem Blog erwähnte Methode verwendet, sondern Symbol direkt verwendet. Babel hat eine Kompatibilitätsverarbeitung für Symbol() vorgenommen. Wenn es sich um einen Browser handelt, der Symbol unterstützt, wird ES6-Symbol direkt verwendet. Wenn es nicht unterstützt wird, wird stattdessen ein von Babel implementiertes simuliertes Symbol verwendet.
Code, der private Mitglieder hinzufügt, sieht etwas seltsam aus, wie zum Beispiel der folgende Code für die einfache Point-Klasse. Die folgende Implementierung dient hauptsächlich dazu, (so weit wie möglich) sicherzustellen, dass die Koordinaten des Point-Objekts nach der Generierung nicht mehr nach Belieben geändert werden können, d. h. unveränderlich.
const __ = { x: Symbol("x"), y: Symbol("y") }; export default class Point { constructor(x, y) { this[__.x] = x; this[__.y] = y; } get x() { return this[__.x]; } get y() { return this[__.y]; } move(offsetX = 0, offsetY = 0) { return new Point(this.x + offsetX, this.y + offsetY); } }
Modelle
Nur Skripte/Modell Die wenigen unten implementierten Klassen sind relativ reine Modelle, mit Ausnahme von Feldern, die zum Speichern von Daten verwendet werden (Feld) und Neben Eigenschaften für den Zugriff auf Daten werden auch Methoden für den Zugriff auf Daten verwendet.
Point 和 BlockPoint,继承
model/point.js 和 model/blockpoint.js 里分别实现了用于描述点(小方块)的两个类,区别仅仅在于 BlockPoint 多一个颜色属性。实际上 BlockPoint 是 Point 的子类。在 ES6 里实现继承太容易了,下面是这两个类的结构示意
class Point { constructor(x, y) { // .... } }class BlockPoint extends Point { constructor(x = 0, y = 0, c = "c0") { super(x, y); // .... } }
继氶的实现关键就两点需要注意:
通过 extends 关键字实现继承
如果子类中定义了构造函数 constructor,记得第一句话一定要调用父类的构造函数 super(...)。Javaer 应该很熟悉这个要求的。
Form
Form 在这里不是“表单”的意思,而是“形状、外形”的意思,表示一个方块图形(Shape)通过旋转形成的最多4 种形态,每个 Form对象是其中一种。所以 Form 其实是一组 Point 组成的。
上一个版本中没有定义 Form 这个数据结构,是在生成 Shape 的时候生成的匿名对象。那段代码看起来特别绕,虽然也可以提取个函数出来,不过现在通过 Form 类的构造函数来生成,不仅达到了同样的目的,也把 width 的 height 封装起来了。
Shape 和 SHAPES
Shape 和 SHAPES 跟原来区别不大。SHAPES 的生成代码通过定义 Form 类,简化了不少。而 Shape 类在构建后,也由于成员私有化的原因,color 和 forms 不能被改变了,只能获取。
Tetris 中的游戏相关类
除了几个比较纯粹的模型类放在 model 中,主要入口 index.js 和 tetris.js 放在脚本源码根目录下,其它的游戏相关类都是放在tetris 目录下的。这只是用包(Java概念)或命名空间(C++/C#概念)的概念对源码进行了一个基本的划分。
Block 和 BlockFactory
Block 表示一个大方块,是由四个小方块组成的大方块,它的原型(此原型非 JS 的 Prototype)就是 Shape。所以一个 Block 会有一个 Shape 原型的引用,同时保存着当前它的位置 position 和形态 formIndex,这两个属性在游戏过程中是可以改变的,直接影响着 Block 最终绘制出来的位置和样子。
整有游戏中其实只有两个 Block,一个在预览区中,另一个在游戏区定时下落并被玩家操作。
Block 对象下落到底之后就不再是 Block 了,它会被固化在游戏区。为什么要这样设计呢?因为 Block 表示的是一个完整的大方块,而游戏区下方的方块一旦填满一行就会被消除,大方块将再也不完整。这种情况有两个方案可以描述:
仍然以大方块对象放在那里,但是标记已被消除的块,这样在绘制的时候就可以不绘制已消除的块。
大方块下落完成之后就将其打散成一个个的 BlockPoint,通过矩阵管理。
很明显,第二种方法通过二维数组实现,会更直观,程序写起来也会更简单。所以我选用了第二种方法。
Block 除了描述大方块的位置和形态之外,也会配合游戏控制进行一些数据运算和变化,比如位置的变化:moveLeft()、moveRight()、moveDown() 等,以及形态的变化 rotate();还有几个 fastenXxxx 方法,生成BlockPoint[] 用于绘制或判断下一个位置是否可以放置。关于这一点,在 JavaScript 版俄罗斯方块 中已经谈过。
BlockFactory 功能未变,仍然是产生一个随机方块。
Puzzle 和 Matrix
之前对 Puzzle 和 Matrix 的定义有点混淆,这里把它们区分开了。
Puzzle 用于绘制浏览区和预览区,它除了描述一个指定长宽的绘制区域之外,还有存储着两个重要的对象,block: Block 和fastened: BlockPoint[],也就是上面提到的运动中的方块,和固定下来的若干小方块。
Puzzle 本向不维护 block 和 fastened,但它要绘制这两个重要数据对象中的所有 BlockPoint。
Matrix 不再是一个类,它是两个数据。一个是 Puzzle 中的 matrix 属性,维护着由
由于 Tetris::matrix 在大部分时间是不变的,则 Puzzle 绘制的时候需要的只是其中其中非空部分的列表,所以这里有一个比较好的业务逻辑是:在 Tetris::matrix 变化的时候,从它重新生成 Puzzle::fastened,由 Puzzle 绘制时使用。
Eventable
在重构和写新功能的过程中,发现了事件的重要性,好些处理都会用到事件。
比如在点击暂停/恢复 和 重新开始 的时候,需要去判断当前游戏的状态,并根据状态的情况来触发到底是不是真的暂停或重新开始。
又比如,在计分和速度选择功能中,如果计分达到一定程度,就需要触发提速。
上面提到的这些都可以使用观察者模式来设计,则事件就是观察者模式的一个典型实现。要实现自己的事件处理机制其实不难,但是这里可以偷偷懒,直接借用 jQuery 的事件处理,所以定义了 Eventable 类用于封装 jQuery 的事件处理,所有支持事件的业务类都可以从它继承。
封装很简单,这里采用的是封装事件代理对象的方式,具体可以看源代码,一共只有 20 多行,很容易懂。也可以在构造函数中把this 封装一个 jQuery 对象出来代理事件处理,这种方式可以将事件处理函数中的 this 指向自己(自己指 Eventable 对象)。不过还好,这个项目中不需要关心事件处理函数中的 this。
StateManager
在实现 Tetris 中的主要游戏逻辑的时候,发现状态管理并不简单,尤其是加了 暂停/恢复 按钮之后,暂停状态就分为代码暂停和人工暂停两种情况,对于两种情况的恢复操作也是有区别的。除此之外还有游戏结束的状态……所以干脆就定义个 StateManager 来管理状态了。
StateManager 维护着游戏的状态,提供改变状态的方法,也提供判断状态的属性。如果 JavaScript 有接口语法的话,这个接口大概是这样的
interface IStateManager { get isPaused(): boolean; get isPausedByManual(): boolean; get isRestartable(): boolean; get isOver(): boolean; pause(byWhat); resume(byWhat); start(); over(); }
InfoPanel 和 CommandPanel
InfoPanel 主要用于积分和速度的管理,包括与用户的交互(UI)。CommandPanel 则是负责两个按钮事件的处理。
Tetris
说实在的,我仍然认为 Tetris 的代码有点复杂,还需要重构简化。不过尝试了一下之后发现这并不是一件很容易的事情,所以就留待后面的版本来处理了。
小结
Diese Rekonstruktion des Tetris-Spiels ist nur eine vorläufige Rekonstruktion. Der ursprüngliche Zweck besteht nur darin, das Modell klar zu definieren, aber sie spaltet auch die Geschäftsverarbeitung auf. Der Zweck der Modelldefinition wurde erreicht, die Geschäftsaufteilung ist jedoch immer noch nicht zufriedenstellend.
Die beiden vorherigen Projekte verwendeten TypeScript 1.8. Obwohl TypeScript 1.8 einige Fallstricke aufweist, sind die statischen Sprachfunktionen von TypeScript, insbesondere die statische Überprüfung, immer noch sehr hilfreich für große JavaScript-Projekte. Früher dachte ich, dass TypeScript die Codemenge erhöht und die Flexibilität von JavaScript verringert, aber dieses Mal hatte ich das tiefe Gefühl, dass dies kein Manko von TypeScript ist. Es kann dieses Problem zumindest lösen in JavaScript. Ein paar Fragen:
Statische Inspektion kann viele potenzielle Probleme während der Entwicklungsphase und nicht während der Laufzeit finden. Denn je früher ein Problem erkannt wird, desto einfacher ist es, es zu beheben.