带绘图的Mario5

分享于 

44分钟阅读

Web开发

  繁體

Mario5 TypeScript

内容

简介

CodeProject的一个最辉煌的时刻是关于Mario5的文章的发布。 本文介绍了基于web技术的游戏的制作,如 HTML5.CSS3和 JavaScript。 这篇文章获得了很多 attentation,可以能是我真正令人荣誉的它的中之一。

最初的文章使用了我描述为"oop javascript"的东西。 我编写了一个叫做 oop.js,的小 helper 脚本,它允许我使用简单的inheritence/class Pattern。 当然,JavaScript从开始就非常面向对象。 类不是OOP的直接标准。 然而,Pattern 帮助了大量的代码,易于阅读和维护。 这是由非 Having 来直接处理 prototype 方案得到的。

使用 app,我们在JavaScript中得到一个统一的类构造。 语法基于ES6版本,即使使用ES6实现,也可以准备字典以保持完整的JavaScript超集。 当然,TypeScript编译到ES3或者 ES5,这意味着该类构造将被分解为目前可用的东西: 然而,仍然是 prototype的代码,它是可以读的。跨实现( es3/es5 ) 安全的,同时也是一个共同的。 除了我自己的方法( oop.js ) 没有人知道在不阅读 helper 代码的情况下发生了什么。 使用 app,大量开发人员使用相同的Pattern,因为它嵌入在语言中。

因此,将Mario5项目转换为app是自然的。 什么使它值得一篇关于CodeProject的文章? 我认为如何转换一个项目是一个很好的研究。 它还说明了打字的要点。 它对语法和行为做了一个很好的介绍。 毕竟,对于已经知道JavaScript的人来说,简历很容易,对于那些没有任何经验的人来说更容易。

背景

超过一年前,Anders宣布新的微软语言调用应用程序。 这对大多数人来说非常令人惊讶,因为,( 尤其是 Anders ) 似乎针对动态语言,特别是 JavaScript。 但是,微软认识到,集中通用编程的大机会是对web编程的重要性。 JavaScript存储应用程序,使用JavaScript和使用JavaScript来运行查询的文档存储,显然很重要,这是一种非常重要的做法。

注意:在开发过程中,在某个点( v0.8 ) 加入了。 他是否发明了这种语言,或者是否有人提出了这个想法。 然而,这个团队目前已经领导了他的经验和专业知识,当然让他负责项目。

实现对新语言设计的影响。 to决定任何可以能建立的语言都可以扩展 JavaScript,而不是从( 就像谷歌用 Dart 做的一样) 创建新语言。 无任何解决办法。 CoffeeScript的问题在于它隐藏了 JavaScript。 对于某些开发人员来说,这可能是吸引人的,但对于大多数开发人员来说,这是一个绝对的排除标准。 Anders认为语言必须是强类型的,即使只有智能编译器( 或者transpiler更正确) 才能看到这些注释。

那么发生了什么已经创建了 ECMAScript 5的true 超集。 这个超集被称为 TypeScript,以指示关闭relationsship与 JavaScript ( 一般情况下),以及额外的类型注释。 其他所有功能,例如接口,枚举,泛型,强制转换。 从这些类型注释后。 未来的TypeScript将发展。 有两个区域:

  • 将ES6保留为JavaScript的true 超集
  • 引入更多的功能使JS开发更容易

使用TypeScript的主要好处是两个折页。 一方面,我们可以充分利用编译时可能出现的错误和问题的信息。 如果参数不满足给定的签名,则编译器将引发错误。 当使用较大的团队或者较大的项目时,这一点尤其有用。 另一边也很有趣。 微软因它的优秀的Visual Studio 工具而闻名。 由于JavaScript代码的动态特性,为JavaScript代码提供良好的工具支持是很乏味的。 因此,即使是简单的重构任务,如重命名变量,也不能用期望的稳定性来执行。

在最终的文稿中,我们提供了大量的工具支持,并且与我们的代码如何工作。 生产力和健壮性的结合是使用应用程序的最吸引人。 在本文中,我们将探讨如何转换现有项目。 我们将看到,将代码转换为app可以增量地完成。

转换现有项目

TypeScript不隐藏 JavaScript。 它以普通JavaScript开始。

JavaScript logo

利用TypeScript的第一步当然要有打字源文件。 由于我们希望在现有项目中使用 app,所以我们必须转换这些文件。 这里没有什么需要做的,但是我们只需将文件从 *.js 重命名为 *.ts. 这只是约定的问题,实际上不是必需的。 然而,作为TypeScript编译器通常认为文件作为输入,编写 *.js 文件作为输出,重命名扩展确保不会出现错误的错误。

接下来的小节将处理转换过程中的增量改进。 我们现在假设每个文件都有通常的字幕扩展名 *.ts,,即使没有使用任何额外的。

引用

第一步是提供从单个JavaScript文件到所有其他( 必选) JavaScript文件的引用。 通常我们只会编写单个文件,但是( 通常) 必须在我们的HTML代码中以特定的顺序插入。 JavaScript文件不知道HTML文件,也不知道这些文件的顺序。

现在我们希望给智能编译器( 打字) 提供一些提示,我们需要指定它的他对象可以能可以用。 因此,我们需要在代码文件的开头放置一个引用提示。 引用提示将声明所有其他文件,这些文件将从当前文件中使用。

例如我们可以通过它的定义包括 jQuery ( 由 比如 使用,main.ts 文件):

///<referencepath="def/jquery.d.ts"/>

然而,我们还可以包括一个图书簿版本的库,或者JavaScript版本,但是包括定义文件的原因。 定义文件不包含任何逻辑。 这将使文件substentially更小更快。 此外,这些文件通常包含更多/更好的文档注释。 在 finally 中,当我们将自己的*.ts 文件更喜欢 *.d.ts 文件时,在jQuery和其他库的情况下,原始文件已经。 如果应用程序编译器对源代码感到满意,则不清楚。 通过定义一个定义文件,我们可以确保一切都正常。

有理由编写普通定义文件 outselves,也有。 def/接口。d 文件覆盖了最基本的内容。 我们没有任何代码,这将使编译变得不相关。 另一方面引用这个文件是有意义的,因为文件提供的附加类型信息有助于注释我们的。

注释

最重要的特征是类型注释。 实际上,语言的NAME 表示这个特性的高度重要性。

大多数类型注释实际上不需要。 如果立即指定了变量( 例如。 我们定义了一个变量,而不是声明它,然后编译器可以推断变量的类型。

var basepath = 'Content/';

显然,这个变量的类型是 string。 这也是打字推断的结果。 但是,我们也可以显式地输入类型。

var basepath: string = 'Content/';

通常我们不希望有这样的注释。 它引入更多的混乱和 LESS 灵活性,比我们。 然而,有时需要这样的注释。 当然,当我们只声明一个变量时,出现最明显的情况:

var frameCount: number;

还有其他的场景。 考虑创建单个对象,该对象可以通过更多的属性进行扩展。 编写通常的JavaScript代码对于编译器来说是没有足够的信息的:

var settings = { };

有哪些属性可用属性的类型是什么? 也许我们不知道,我们想把它当作字典。 在这种情况下,我们应该指定对象的任意用法:

var settings: any = { };

但也有另一个情况。 我们已经知道哪些属性可用,而且我们只需要设置或者获取一些可选属性。 在这种情况下,我们还可以指定精确的类型:

var settings: Settings = { };

最重要的情况已经被忽略了。 大多数情况下都可以推断变量( 局部或者全局),但不能推断函数参数。 实际上,函数参数可以为单个使用( 例如泛型参数的类型) 推断,但不能在函数本身中推断。 因此我们需要告诉编译器我们有什么类型的参数。

setPosition(x: number, y: number) {
 this.x = x;
 this.y = y;
}

因此,通过使用类型注释增量转换JavaScript是一个过程,它首先改变函数的签名。 那么这些注释的基础是什么? 我们已经了解到 numberstringany 是内置的类型,它们表示基本类型。 另外我们还有 booleanvoid。 后者只对函数的返回类型有用。 它指示没有任何有用的( 由于JS函数将总是返回一些东西,至少 undefined ) 返回。

数组标准 array的类型为 any[]。 如果我们想指出只有数字可以与 array 一起使用,我们可以将它作为 number[] 进行注释。 多维数组也是可能的。 一个矩阵可能被注释为 number[][]。 由于JavaScript的特性,我们只对多维数组有交错数组。

枚举

现在,我们开始注释函数和变量,最终我们将需要定制类型。 当然,我们已经有了一些类型,但这些类型可以能是 LESS 注释的,或者是以太特殊的方式定义的。

有时,TypeScript提供更好的替代方案。 例如数值常量集合可以定义为枚举。 在旧代码中,我们有如下对象:

var directions = {
 none: 0,
 left: 1,
 up: 2,
 right: 3,
 down: 4};

不明显的是所包含的元素应该是常数。 它们很容易被改变。 那么如果我们真的想用这样一个对象来做令人讨厌的事情,那么编译器会给我们一个错误? 这就是 enum 类型的用武之地。 目前,它们只限于数字,但是对于大多数常量集合,这是足够的。 最重要的是,它们作为类型传输,这意味着我们可以在类型注释中使用它们。

NAME 已经改为大写,这表明 Direction 确实是一个类型。 由于我们不希望像枚举标志那样使用它,所以我们使用单数版本( 遵循. NET 约定,这在这个场景中是有意义的)。

enum Direction {
 none = 0,
 left = 1,
 up = 2,
 right = 3,
 down = 4,
};

现在,我们可以在代码中使用它,如:

setDirection(dir: Direction) {
 this.direction = dir;
}

请注意,dir 参数被注释为限定于 Direction 类型的参数。 这将排除任意数字,并且必须使用 Direction 枚举的值。 如果我们有一个正好是数字的用户输入? 在这样的场景中,我们也可以变得狂野,并使用 TypeScript:

var userInput: number;//.. .setDirection(<Direction>userInput);

只有在可以工作的情况下,才能在绘图中工作。 因为每个 Direction 都是数字,所以 number 可以是有效的Direction。 有时已知转换失败的原因是。 如果 userInput 是普通 string,则TypeScript会抱怨并在转换时返回错误。

命令行接口

接口定义类型而不指定实现。 它们将完全消失在得到的JavaScript中,就像我们的所有类型注释一样。 基本上,它们与 C# 中的接口非常相似,但是有一些显著的。

让我们来看看一个示例界面:

interface LevelFormat {
 width: number;
 height: number;
 id: number;
 background: number;
 data: string[][];
}

这定义了级别定义的格式。 这类定义必须由 widthheightbackgroundid 等数字组成。 另外,一个二维字符串数组定义了应该在级别中使用的各种瓦片。

我们已经提到,app接口与 C# 接口不同。 其中一个原因是,打字接口允许合并。 如果具有给定 NAME的接口已经存在,则不会覆盖它。 也没有编译器警告或者错误。 将使用新接口中定义的属性扩展现有接口。

下面的接口将现有的Math 接口( 从TypeScript基础定义) 与提供的接口合并。 我们获得了一个额外的方法:

interface Math {
 sign(x: number): number;
}

方法是通过在圆括号中指定参数来指定的。 通常的类型注释是方法的返回类型。 通过提供的接口( 扩展),app编译器允许我们编写以下方法:

Math.sign = function(x: number) {
 if (x >0)
 return1;
 elseif (x <0)
 return -1;
 return0;
};

app接口中另一个有趣的选项是混合声明。 在JavaScript中,对象不限于纯 key-value 载体。 也可以将对象作为函数调用。 这种行为的一个很好的例子就是 jQuery。 调用jQuery对象有许多可能的方法,每个方法都会返回一个新的jQuery选择。 jQuery对象还带有表示漂亮小帮手和更有用的东西的属性。

在jQuery中,其中一个接口看起来像:

interface JQueryStatic {
 (): JQuery;
 (html: string, ownerDocument?: Document): JQuery;
 ajax(settings: JQueryAjaxSettings): JQueryXHR;
 /*.. . */}

在这里我们必须调用( 在众多之间) 和一个直接可用的属性。 混合接口因此要求实现对象实际上是一个函数,这是通过进一步的属性扩展的。

我们还可以基于( 或者类,这些类将作为这里上下文中的接口使用)的其他接口创建接口。

让我们考虑以下情况。 要区分点,我们使用 Point 接口。 这里我们只声明两个坐标,xy。 如果我们想在代码中定义图片,我们需要两个值。 放置位置( 偏移量)的位置,以及表示图像源的字符串。

因此我们定义了接口来表示这个功能,它是 Point 接口的派生/专用。 我们使用 extends 关键字在app中触发这里行为。

interface Point {
 x: number;
 y: number;
}interface Picture extends Point {
 path: string;
}

我们可以使用任意数量的接口,但是我们需要用逗号分隔它们。

现在我们已经输入了大部分代码,但是一个重要的概念还没有被翻译成 TypeScript。 原始代码库使用一个特殊的概念来引入类类( 包括。 继承)。最初类似于下面的示例:

var Gauge = Base.extend({
 init: function(id, startImgX, startImgY, fps, frames, rewind) {
 this._super(0, 0);
 this.view = $('#' + id);
 this.setSize(this.view.width(), this.view.height());
 this.setImage(this.view.css('background-image'), startImgX, startImgY);
 this.setupFrames(fps, frames, rewind);
 },
});

不幸的是,所显示的方法有很多问题。 最大的问题是它是不规范的,换句话说,不是标准的方法。 因这里,不熟悉象对象这样的类实现类的开发人员不能读或者写代码。 精确的实现也是未知的。 所有开发人员都必须查看 Class 对象的原始定义及其用法。

使用统一的方法创建类类的统一方法。 另外,它以与 ECMAScript 6相同的方式实现。 因此,我们得到了一个可移植性。可读性和可以扩展性,易于使用和标准化。 从我们原来的示例返回,我们可以将它的转换为:

class Gauge extends Base {
 constructor(id: string, startImgX: number, startImgY: number, fps: number, frames: number, rewind: boolean) {
 super(0, 0);
 this.view = $('#' + id);
 this.setSize(this.view.width(), this.view.height());
 this.setImage(this.view.css('background-image'), startImgX, startImgY);
 this.setupFrames(fps, frames, rewind);
 }
};

这看起来相当相似,而且几乎相同。 然而,需要在单一迭代中修改与in变体的前一个定义。 为什么如果改变基类( 叫 Base ),我们需要更改所有派生类(。app需要从其他应用程序类到 inherit的类)。

另一方面,如果我们改变了派生类中的一个,我们就不能再使用基类了。 这就是说,只有类与类层次完全分离,才能在单一迭代中转换。 否则我们需要把整个班级的层次转换。

extends 关键字与接口具有不同的含义。 接口通过指定的定义集扩展它的他定义( 接口或者类的接口部分)。 类通过将它的Prototype设置为给定的类来扩展另一个类。 另外还有一些它的他整洁的特性,比如通过 super 访问父功能的能力。

最重要的类是hierachy的root,叫做 Base。 它包含了相当多的特性,

class Base implements Point, Size {
 frameCount: number;
 x: number;
 y: number;
 image: Picture;
 width: number;
 height: number;
 currentFrame: number;
 frameID: string;
 rewindFrames: boolean;
 frameTick: number;
 frames: number;
 view: JQuery;
 constructor(x: number, y: number) {
 this.setPosition(x || 0, y || 0);
 this.clearFrames();
 this.frameCount = 0;
 }
 setPosition(x: number, y: number) {
 this.x = x;
 this.y = y;
 }
 getPosition(): Point {
 return { x : this.x, y : this.y };
 }
 setImage(img: string, x: number, y: number) {
 this.image = {
 path : img,
 x : x,
 y : y
 };
 }
 setSize(width, height) {
 this.width = width;
 this.height = height;
 }
 getSize(): Size {
 return { width: this.width, height: this.height };
 }
 setupFrames(fps: number, frames: number, rewind: boolean, id?: string) {
 if (id) {
 if (this.frameID === id)
 returntrue;
 this.frameID = id;
 }
 this.currentFrame = 0;
 this.frameTick = frames? (1000/fps/setup.interval) : 0;
 this.frames = frames;
 this.rewindFrames = rewind;
 returnfalse;
 }
 clearFrames() {
 this.frameID = undefined;
 this.frames = 0;
 this.currentFrame = 0;
 this.frameTick = 0;
 }
 playFrame() {
 if (this.frameTick && this.view) {
 this.frameCount++;
 if (this.frameCount >= this.frameTick) { 
 this.frameCount = 0;
 if (this.currentFrame === this.frames)
 this.currentFrame = 0;
 var $el = this.view;
 $el.css('background-position', '-' + (this.image.x + this.width * ((this.rewindFrames? this.frames - 1 : 0) - this.currentFrame)) + 'px -' + this.image.y + 'px');
 this.currentFrame++;
 }
 }
 }
};

implements 关键字类似于在 C# 中实现接口( explicitely )。 我们基本上启用了一个合同,我们提供了在我们类的给定接口中定义的能力。 虽然我们只能从单个类扩展,但是我们可以实现任意数量的接口。 在前面的示例中,我们选择不从任何类中 inherit,而是实现两个接口。

然后我们定义在给定类型的对象上可用的字段类型。 顺序无关紧要,但最初定义它们( 最重要的是: 在一个地方,有意义。 constructor 函数是一个特殊函数,它的含义与自定义 init 方法的含义相同。 我们将它作为类的构造函数。 类的基构造函数可以通过 super() 调用任何时间。

TypeScript也提供修饰符它们不包含在 ECMAScript 6标准中。 因此我也不喜欢使用它们。 不过,我们可以将字段设置为 private ( 但记住: 只从编译器的视图中,不在JavaScript代码本身中,因此限制了对这些变量的访问。

可以结合构造函数本身,很好地使用这些修饰符:

class Base implements Point, Size {
 frameCount: number;
 // no x and y image: Picture;
 width: number;
 height: number;
 currentFrame: number;
 frameID: string;
 rewindFrames: boolean;
 frameTick: number;
 frames: number;
 view: JQuery;
 constructor(public x: number, public y: number) {
 this.clearFrames();
 this.frameCount = 0;
 }
 /*.. . */}

通过指定参数为 public,我们可以省略类中 xy的定义( 初始化)。 打字将自动处理这里问题。

Fat函数

任何人都记得在lambda表达式之前如何在 C# 中创建匿名函数? 大多数( C# ) 开发人员都不能,原因很简单: Lambda表达式带来表现力和可读性。 在JavaScript中,一切都围绕着匿名函数的概念展开。 个人而言,我只使用 function expressions (anonymous functions) 而不是 function statements (named functions)。 更明显的是发生了什么,更加灵活,并给代码带来一致的外观和感觉。 我认为它是一致的。

然而,还有一些小片段,它很糟糕地写了一些类似的东西:

var me = this;
me.loop = setInterval(function() {
 me.tick();
}, setup.interval);

为什么这些垃圾四行。 第一行是必需的,因为该间隔回调是代表 window 调用的。 因此,我们需要缓存原始 this,以便访问/查找对象。 这个闭包是有效的。现在我们将 this 存储在 me 中,我们可以从较短的输入( 至少有些东西) 中。 finally 我们需要把这个函数放在另一个函数中。 疯狂我们用那个fat函数? !

this.loop = setInterval(() =>this.tick(), setup.interval);

好吧,这只是个 liner。 一行我们通过保持"丢失"在 this 箭头函数( 我们把它们叫做lambda表达式) 中的。 我们专门使用了两行代码来保存函数样式,现在我们使用lambda表达式来实现。 在我看来,这不仅可以读,而且可以理解。

当然,在引擎盖下,TypeScript使用的是与我们之前一样的东西。 但我们不关心。 对于由 C# 编译器生成的MSIL,或者任何C 编译器生成的汇编代码,我们也不关心。 我们只关心( 原) 源代码更易读和灵活。 如果我们不确定 this,那么应该使用fat箭头操作符。

扩展项目

app编译到( 人类可读) JavaScript。 它以 ECMAScript 3或者 5结尾,具体取决于目标。

TypeScript logo

现在,我们基本上输入了整个解决方案,甚至可以进一步使用一些打字功能,使代码更好,更易于扩展和使用。 在我们将看到,app提供了一些有趣的概念,允许我们完全分离应用程序并使它的可以访问,不仅在浏览器中,而且在 node.js ( 因此终端) 等平台上。

默认值和可选参数

现在我们已经很好了,但是为什么要把它放在? 让我们为一些参数设置默认值,使它们成为可选的。

例如将转换以下应用程序 Fragment。

var f = function(a: number = 0) {
}
f();

"。"。到这个:

var f = function (a) {
 if (a === void0) { 
 a = 0; 
 }
};
f();

void 0 基本上是 undefined的安全变种。 这样,默认值总是动态绑定,而不是 C# 中的默认值,这是静态绑定的。 我们现在可以省略所有默认值检查并让字典执行工作。

作为一个示例,请考虑下面的代码段:

constructor(x: number, y: number) {
 this.setPosition(x || 0, y || 0);
 //.. .}

我们为什么要确保 xy 值被设置? 我们可以直接将这个约束放在构造函数函数上。 让我们看看更新后的代码是怎样的:

constructor(x: number = 0, y: number = 0) {
 this.setPosition(x, y);
 //.. .}

还有其他的例子。 在更改后,下面已经显示了该函数:

setImage(img: string, x: number = 0, y: number = 0) {
 this.view.css({
 backgroundImage : img? c2u(img) : 'none',
 backgroundPosition : '-' + x + 'px -' + y + 'px',
 });
 super.setImage(img, x, y);
}

同样,这使得代码更易于阅读。 否则,backgroundPosition 属性将被赋予默认值考虑,这看起来很难看。

有默认值肯定很好,但是我们可能有一个场景,在这里我们可以安全地省略参数,不需要 Having 来指定默认值。 在这种情况下,我们仍然要检查是否提供了参数,但是调用者可以忽略参数。

关键是在参数中放置问号 behind。 让我们来看看一个例子:

setupFrames(fps: number, frames: number, rewind: boolean, id?: string) {
 if (id) {
 if (this.frameID === id)
 returntrue;
 this.frameID = id;
 }
 //.. .returnfalse;
}

显然,我们允许调用该方法而不指定 id 参数。 因此我们需要检查它是否存在。 这是在方法的body的第一行中完成的。 这个保护保护可选参数的用法,即使TypeScript允许我们自由使用它。 不过我们应该小心点。 TypeScript将不会检测到所有错误- 它仍然是我们的责任,确保一个工作代码的每一个可以能的路径。

重载

JavaScript本身不知道函数重载。 原因很简单: 命名函数只会导致局部变量。 将函数添加到对象将在它的字典中放置一个键。 两种方式只允许唯一标识符。 否则我们将允许具有相同 NAME的两个变量或者属性。 当然这里有一个简单的方法。 根据参数的数量和类型,我们创建了一个调用子函数的超级函数。

然而,检查参数的数量很容易,获取类型是困难的。 至少在编译时知道/保留类型,然后将整个创建的类型系统抛出。 这意味着在运行时不可能进行类型检查- 至少不超过非常基本的JavaScript类型检查。

好的,那么为什么一个专门专门用于这个主题的部分,当文件夹不帮助我们这里时? 显然,编译时间重载仍然是可能的和需要的。 许多JavaScript库提供提供一个或者另一个功能的函数,具体取决。 例如jQuery通常提供两个或者更多。 一种是读,另一种是写一个。 当我们在TypeScript中重载方法时,我们只有一个具有多个签名的实现。

通常,人们试图避免这样的歧义定义,这就是为什么原始代码中没有这样的方法。 我们现在不想介绍它们,但是让我们看看如何写它们:

interface MathX {
 abs: {
 (v: number[]): number;
 (n: number): number;
 }
}

实现可以如下所示:

var obj: MathX = {
 abs: function(a) {
 var sum = 0;
 if (typeof(a) === 'number')
 sum = a * a;
 elseif (Array.isArray(a))
 a.forEach(v => sum += v * v);
 return Math.sqrt(sum);
 }
};

告诉TypeScript调用多个调用版本的优点在于增强的UI能力。 像 Visual Studio 这样的ide或者文本编辑器可能会显示所有的重载,包括描述。 通常,调用只限于提供的重载,这将确保某些安全。

泛型

泛型对于驯服多个( 类型) 用法也很有用,也可以。 它们和 C# 一样有点不同,因为它们只在编译时被评估。 另外,它们没有关于运行时表示的任何特殊。 这里没有模板元编程或者任何东西。 泛型只是处理类型安全的另一种方法,而不会变得过于冗长。

让我们考虑以下函数:

function identity(x) {
 return x;
}

这里的参数 x的类型为 any。 因此函数将返回类型为 any的东西。 这可能不是一个问题,但是让我们假设下面的函数调用。

var num = identity(5);var str = identity('Hello');var obj = identity({ a : 3, b : 9 });

numstrobj的类型是什么? 它们可以能有一个明显的NAME,但从字幕编译器的角度来看,它们都是 any 类型。

这就是泛型来拯救的地方。 我们可以教编译器,函数的返回类型是调用类型,该类型应该是已经使用的精确类型。

function identity<t>(x: T): T {
 return x;
}</t>

在 上面 Fragment中,我们只返回已经输入函数的相同类型。 有多种可能性( 包括返回从上下文确定的类型),但是返回其中一个参数类型是最常见的。

当前代码没有包含任何泛型。 原因很简单:代码主要集中在更改状态,而不是评估输入。 因此我们主要处理过程而不是函数。 如果我们使用具有多种参数类型的函数,具有参数类型依赖或者类似构造的类,那么泛型肯定很有用。 现在一切都有可能没有他们。

模块

final touch是为了分离我们的应用程序。 我们不引用所有文件,而是使用 MODULE 加载器( 比如。 AMD浏览器,或者CommonJS用于节点,并按需加载各种脚本。 这个 Pattern 有很多优点。 代码更容易测试。调试和通常不会出错,因为模块总是在指定的依赖项之后加载。

to提供整个 MODULE 系统的整体抽象,因为它提供了两个关键字( importexport ),它被转换为所需的MODULE 系统。 这意味着可以将单个代码库编译为AMD兼容代码,以及CommonJS的一致代码。 不需要魔法。

作为一个例子,文件 constants.ts 不再是referened了。 文件将以 MODULE 格式导出它的内容。 这是通过以下方式完成的:

export var audiopath = 'Content/audio/';
export var basepath = 'Content/';
export enum Direction {
 none = 0,
 left = 1,
 up = 2,
 right = 3,
 down = 4,
};/*.. . */

如何使用这里方法? 我们使用 require() 方法代替 Having 引用注释。 表示我们希望直接使用 MODULE,但不写入 var,而是 import。 请注意,我们可以跳过 *.ts 扩展。 这是有意义的,因为文件后面将有相同的NAME,但是不同的结尾。

import constants = require('./constants');

varimport 之间的区别是相当重要的。 请考虑以下行:

import Direction = constants.Direction;
import MarioState = constants.MarioState;
import SizeState = constants.SizeState;
import GroundBlocking = constants.GroundBlocking;
import CollisionType = constants.CollisionType;
import DeathMode = constants.DeathMode;
import MushroomMode = constants.MushroomMode;

如果我们要编写 var,那么我们实际上将使用属性的JavaScript表示形式。 但是,我们想使用TypeScript抽象。 Direction的JavaScript实现只是一个对象。 TypeScript抽象是一种类型,它将以对象的形式实现。 但是,有时它并没有区别,比如接口。类或者枚举,我们应该更喜欢 import,而不是 var。 否则,我们只需使用 var 进行重命名:

var setup = constants.setup;var images = constants.images;

这是一切嗯,对模块有很多要说的,但我在这里尽量简单。 首先,我们可以使用这些模块对文件进行接口。 例如 main.ts的public 接口由以下代码段给出:

export function run(levelData: LevelFormat, controls: Keys, sounds?: SoundManager) {
 var level = new Level('world', controls);
 level.load(levelData);
 if (sounds)
 level.setSounds(sounds);
 level.start();
};

我们将所有模块放在 game.ts. 之类的文件中,我们加载所有依赖项,然后运行游戏。 尽管大多数模块只是与单个零件绑定在一起的对象,但 MODULE 也可以只是这些片段的一。

import constants = require('./constants');
import game = require('./main');
import levels = require('./testlevels');
import controls = require('./keys');
import HtmlAudioManager = require('./HtmlAudioManager');
$(document).ready(function() {
 var sounds = new HtmlAudioManager(constants.audiopath);
 game.run(levels[0], controls, sounds);
});

controls MODULE 是单个片 MODULE的例子。 我们用一条语句实现这里目的,如:

export = keys;

export 对象指定为 keys 对象。

让我们看看现在有什么。 由于我们代码的模块化特性,我们包含了一些新文件。

TypeScript restructure

我们对RequireJS有一个依赖,但是事实上我们的代码比以前更健壮,更易于扩展。 此外,所有依赖项始终被公开,这将彻底消除未知依赖项的可能性。 MODULE 加载系统结合智能感知。改进重构能力和强大的类型,给整个项目增加了很多安全。

当然并不是每个项目都能很容易地。 项目已经很小,并且基于一个实体的代码库,这并不是 Rust的。

在 final 步骤中,我们将分解大量的main.ts 文件,以创建小的。分离的文件,这只取决于某些。 这里设置将在开始时被注入。 然而,这样的转变并非针对每个人。 对于某些项目,它可能会增加太多的噪音,而不是获得 clearity。

不管怎样,对于 Matter 类,我们都有以下代码:

///<referencepath="def/jquery.d.ts"/>import Base = require('./Base');
import Level = require('./Level');
import constants = require('./constants');class Matter extends Base {
 blocking: constants.GroundBlocking;
 level: Level;
 constructor(x: number, y: number, blocking: constants.GroundBlocking, level: Level) {
 this.blocking = blocking;
 this.view = $('<div/>').addClass('matter').appendTo(level.world);
 this.level = level;
 super(x, y);
 this.setSize(32, 32);
 this.addToGrid(level);
 }
 addToGrid(level) {
 level.obstacles[this.x/32][this.level.getGridHeight() - 1 - this.y/32] = this;
 }
 setImage(img: string, x: number = 0, y: number = 0) {
 this.view.css({
 backgroundImage : img? img.toUrl() : 'none',
 backgroundPosition : '-' + x + 'px -' + y + 'px',
 });
 super.setImage(img, x, y);
 }
 setPosition(x: number, y: number) {
 this.view.css({
 left: x,
 bottom: y
 });
 super.setPosition(x, y);
 }
};
export = Matter;

这种技术将改进依赖性。 此外,代码库将获得可以访问性。 然而,这取决于代码的项目和状态,如果进一步精化实际需要或者不必要的化妆品。

使用代码

Mario5 TypeScript

代码是实时的,可以在GitHub在线。 可以通过 github.com/FlorianRappl/Mario5TS 访问存储库。 存储库本身包含一些关于app的信息。 此外,还使用了构建系统 Gulp。 我将在另一个帖子中介绍这个构建系统。 然而,存储库还包含一个简短的安装/使用指南,它应该给每个人一个跳起。

由于代码的原因在于Mario5文章中,我还建议所有没有读过它的人都看看。 本文可以在 codeproject.com/Articles/396959/Mario的CodeProject上找到。 还有一个关于CodeProject的后续文章,它涉及扩展原始源代码。 扩展是一个级别编辑器,它显示了Mario5游戏的设计实际上是很好的。 你可以访问 codeproject.com/Articles/432832/Editor-for-Mario 下的文章。 应该注意的是,本文还讨论了一个社交游戏平台,该平台将游戏和编辑器结合在一起。

兴趣点

原文中最有问题的一个问题是获取声音/如何设置声音系统。 发现声音可以能是最有趣的部分之一,但我决定把它从文章中删除。 为什么?

  • 声音文件可能导致法律问题( 但是,对于图形也可能会出现同样的问题)
  • 声音文件实际上相当大( 效果文件很小,但 background 音乐是 O(MB) )
  • 为了避免兼容性问题( OGG和MP3文件分发),每个声音文件都必须被复制
  • 这个游戏是独立于特定的声音实现的。

最后一个论点是我的重点。 我想说明,这个游戏实际上可以在没有强大的实现的情况下实现。 音频已经成为网络应用领域的一个广泛。 首先,我们需要考虑一系列格式,因为不同格式和编码只在浏览器的子集上工作。 要访问所有主流浏览器,通常需要至少 2种不同格式的( 通常由一个公开的或者一个专有的格式组成的)。 另外,HTMLAudioElement的当前实现对于游戏来说不是非常有效和有用的。 这就是促使谷歌在另一个标准上工作的原因,它对游戏的效果更好。

不过,你需要一个标准实现? GitHub仓库实际上包含一个标准实现。 原始的JavaScript版本是可用的,而类型的版本是。 它们都被称为 SoundManager。 一个位于文件夹原始文件夹中,另一个位于脚本文件夹( 两者都是src的子文件夹) 中。

历史

  • v1.0.0 | 初始发行版|
  • typescript | 18.12.2014历史 v1.1.0 | 注释

相关文章