JavaScript 版俄罗斯方块——转换为 TypeScript

写 JavaScript 版俄罗斯方块的目的是为试验了技术和框架。最初的版本 经过 Gulp + Webpack + Babel,搭建了一个 ES6 的前端构建环境;以后的一个版本 经过重构技术对模型部分进行较全面的重构,同时引入了 私有成员写法,也在重构的过程当中发现,用 TypeScript 来写脚本是个比较好的选择。javascript

下面就开始把 主要工做分支 working 切换为 TypeScript 脚本。html


传送门


引入 TypeScript 环境

安装 TypeScript

若是没有 安装 TypeScript,首先确定是要安装的。TypeScript 我也不是第一次用,此次主要是用新发布的 2.0 版本尝试一下新特性。前端

用 NPM 安装 TypeScript,这在 Visual Studio Code 中会用到,最新版是 2.0.3,因此安装的时候不用加版本标签了。java

npm install typescript

配置 Visual Studio Code

以前有人问 tsc 编译器 2.0.3 与 VScode 代码语言服务 1.8.10 版本不匹配 怎么解决,这里我已经回答过一次如何配置 VSCode 的语言服务,这里再简单的描述一下。node

根据 VSCode 官方文档,须要配置 "typescript.tsdk" 参数,能够在全局 settings.json 中配置,也能够仅为 VSCode 项目配置(.vscode/settings.json)。webpack

首先是找到 TypeScript 安装的位置,用 npm list -g typescript 命令:git

$ npm list -g typescript
C:\Users\james\AppData\Roaming\npm
+-- typescript@2.0.3
`-- typings@1.3.3
  `-- typings-core@1.4.1
    `-- typescript@1.8.7

npm 的位置是 C:/Users/james/AppData/Roaming/npm,后面拼上 node_modules/typescript/lib 就是 TypeScript 语言服务和库的位置了,因此完整的位置是github

C:/Users/james/AppData/Roaming/npm/node_modules/typescript/lib

clipboard.png

为项目引入 TypeScript

以前已经提到,前端项目的源码是放在 src 目录下,因此从控制台进入 src 项目。若是 VSCode 安装了 Start any shell 插件,能够直接在 VSCode 中打开,我我的比较喜欢用 Git Bash。web

在 src 目录下使用 tsc -init 命令,tsc(TypeScript CLI)会建立 tsconfig.json 配置文件。基本上不用改,可是须要咱们加入 "outFile" 选项指定输出目录:typescript

{
    "compilerOptions": {
        "target": "es5",
        "noImplicitAny": false,
        "sourceMap": true,
        "removeComments": true,
        "outFile": "../js/tetris.js"
    },
    "include": [
        "scripts/**/*"
    ]
}

配置好以后直接在 src 目录下就能够经过命令 tsc 编译 ts 脚本。不过这里仍是准备用 gulp 来统一构建,因此配置一下 npm 项目(package.json)。

由于不须要编译 ES6 的 JavaScript,webpack 和 babel 暂时不须要了,因此一并 uninstall 掉。保持开发环境和源码干净是个好习惯。

npm install gulp-typescript
npm uninstall babel-core babel-loader babel-preset-es2015 webpack

随后修改 gulpfile.js,删除 webpack 任务,添加 typescript 任务

gulp.task("typescript", callback => {
    const ts = require("gulp-typescript");
    const tsProj = ts.createProject("tsconfig.json");
    const result = tsProj.src()
        .pipe(sourcemaps.init())
        .pipe(tsProj());
    return result.js
        .pipe(sourcemaps.write("../js", {
            sourceRoot: "../src/scripts"
        }))
        .pipe(gulp.dest("../js"));
});

配置 gulp-typescript 和 sourcemap 仍是花了些时间试验。sourcemap 是参照 less 任务的配置进行了,试验过程当中发现路径配置略有不一样,根据试验结果修正便可。

到此环境基本上就搭好了

JavaScript → TypeScript

虽说 TypeScript 是 JavaScript 的超级,理论上来讲只须要把 .js 改名 为 .ts 就能完成 JavaScript 到 TypeScript 的转换。用 git mv x.js x.ts 把文件名一个个改完以后,发现并非想像的这么简单,编译结果有一大堆错误提示。

GIT 不熟,因此不知道如何批量重命名,只好用 git mv 一我的重命名了,但愿 GIT 高手能指点一二

当时也没去细想,直接就把代码改为了之前习惯的 ts 文件结构,用命名空间把代码都包了一层。如今想来,有多是由于 "target": "es5" 这个选项的缘由,毕竟以前的 JS 源码中用了 ES6 的模块语法,而 TypeScript 虽然能够把 ES6 模块语法转换成 AMD 或者 System 等模块语法,却须要配置。

另外,TypeScript 全部类的数据成员(字段,Field)须要提早申明。这也是形成编译不能经过的缘由之一。

仍然以最小的 Point 为例,看看改造结果

namespace tetris.model {
    export interface IPoint {
        x: number;
        y: number;
    }

    export class Point {
        private _x: number;
        private _y: number;

        constructor(point: IPoint);
        constructor(x: number, y: number);
        constructor(x: any, y?: number) {
            if (y === void 0) {
                this._x = x.x;
                this._y = x.y;
            } else {
                this._x = x;
                this._y = y;
            }
        }

        get x(): number {
            return this._x;
        }

        get y(): number {
            return this._y;
        }

        set(x: number = this._x, y: number = this._y) {
            this._x = x;
            this._y = y;
        }

        move(offsetX: number = 0, offsetY: number = 0): Point {
            return new Point(this.x + offsetX, this.y + offsetY);
        }
    }
}

这段代码用到了命名空间、接口、类、私有属性、重载(overload) 等语言特性,仅于篇幅,就不详述了,TypeScript Documentation 中有详细的教程。

TypeScript 提供了 private 关键字,但最终转换出来的 JavaScript 中,全部 private 属性仍然能够被外部访问,也就是说,TypeScript 的 privateprotected 等修饰词仅用于它本身的语法检查。从减小项目代码自己的的 BUG 这一目的来讲,已经够了。但若是是写类库,考虑到很多用户的 Hacking 天赋,仍是有些欠缺。

本项目不用考虑 Hacking 的问题,因此代码转换的过程当中,全部 Symbol 实现的私有化都换成了 private

TypeScript GitHub Issue 中有人提到但愿转换的代码中用 Symbol 来实现真正的私有化,但通过一群人的 激烈讨论(全英文,有兴趣本身去看吧),被否决了。也许之后 TypeScript 会认真考虑这个问题,但至少如今没实现。

引入模块

定义在同一个命名空间中东西,哪怕是分文件写的,都不须要 import。可是若是是没有 export 的东西,就只能在同一个命名空间块中使用。

这里的 importexport 并非 ES6 模块的语言特性,而是 TypeScript 的语言特性,在这一点上,TypeScript 和 ES6 在语法上很容易混淆,好比 export class 是 TS 语法,也是 ES6 语法,tsc 会根据使用场景不一样来区分,可是 export default class 就是 ES6 语法了,TS 须要配置支持。

import Point = model.Point 这种写法是 TS 的语法,主要用于简化带命名空间的名称,这个和 ES6 的语法差异仍是比较大的,不容易搞混。

不过因而可知一斑,TypeScript 前途漫漫啊。

TypeScript 带来的好处

在 ES6 刚发布先后那段时间,TypeScript 带来的好处之一就是可使用 ES6 的类语法来简化类定义和继承。不过随着 ES6 和 Babel 等工具的普遍使用,这已经再也不是 TypeScript 的优点。

不过从 TypeScript 2.0 的发布说明中,能够感受到 TypeScript 抓住了重点——静态化 JavaScript。对于动态语言最大的问题就是,错误要在运行中去碰见。而静态语言在编译过程就能检查出来几乎全部的语法错误和部分可能的逻辑错误。

即便这个小小的试验性的俄罗斯方块程序,在改写为 TypeScript 的过程当中,也发现了一些问题

自注释代码

我比较推崇写自注释代码——我并非说不该该写注释,而是说,代码变量和方法自己就应该起到必定的注释做用。不少所谓的注释,其实就是把英文的方法和变量名称翻译成中文而,这样的注释,其实没啥做用。

JavaScript 中的自注释只能经过名称来实现,而 TypeScript 中还能够提供类型、重载等信息。好比 Point 构造函数,在 JavaScript 中

constructor(x, y) {
    if (typeof x === "object") {
        x = x.x; y = x.y;
    }
    // ...
}

光从构造函数的申明上来看,彻底不会知道能够传入一个带 xy 属性的对象来代码分别传入 xy。可是 TypeScript 的函数申明就很明白

constructor(point: IPoint);
constructor(x: number, y: number);
constructor(x: any, y?: number) {
    // 这里是实现
}

使用类型的问题

当初定义 Point 类的时候,就是但愿能把它用在项目中,便于之后的重构。而后,改写为 TS 的过程当中却出现了好几个类型不匹配的错误,都是由于直接使用了字符量对象 { x: v1, y: v2 } 这种形式来代替 Point 对象。

忘记了返回值

Block 类的 moveLeft()moveRight()moveDown() 等方法在设计的时候是计划返回 this 以便于链式调用的。不过很不幸,JavaScript 不检查返回值,因此 moveDown 忘了返回。

可是 TypeScript 中若是对方法申明了返回值类型,就会检查回返值,因此这个错误一会儿就被发现了。

空值检查

虽然因为后面提到的坑,最终没有使用 TypeScript 的严格空检查模式。可是这个模式仍然帮助我检查出来几个可能产生空引用错误的地方。真心但愿 TypeScript 能更快的完善,以即可以更普遍的使用这些严格模式来帮助检查错误。

检查未使用的变量和参数

TypeScript 2.0 的这两个选项能够检查未使用的局部变量和参数,这对于净化代码是颇有帮助的。不过由于参数定义有时候是涉及到接口约定,并非说没有在程序中用到就必定没用,因此最终我取消了对未使用参数的检查。

TypeScript 的坑

代码转换过程当中仍是遇到很多坑的

严格空检查模式下不能正确识别 Array.prototype.filter 结果类型

严格空检查模式是 TypeScript 2.0 的新特性,这个模式下 null 是一个独立的数据类型,而不是全部对象类型均可以有 null 值。

在 fasten 操做和删除行操做的时候,都会用到 filter() 来过滤出有效的 BlockPoint 对象,好比

this._puzzle.fastened = this._matrix.reduce((all, row) => {
    return all.concat(row.filter(t => t));
}, []);

这里 this._matrix 是一个 BlockPoint | null 的二维数组,而 Puzzle::fastened 被定义为 BlockPoint 的一维数组,它们的元素类型之间,就是一个 null 类型的区别,很显然,经过 row.filter(t => t) 获得的结果已经不可能包含 null 了,因此结果类型应该是 Array<BlockPoint> 而不是 Array<BlockPoint | null>。然而 TypeScript 2.0 仍然推断为 Array<BlockPoint | null>。在 GitHub Issue 上已经有不少人提出这个问题,估计会在 2.1 中解决。

本项目中,实在不想为这个个事情去写循环处理,因此只好去掉了 "strictNullChecks": true 参数配置,不使用严格空检查模式。

没有自动依赖检查

项目代码编译过了以后,运行时会出现一些类型引用的错误,好比某个类的基类须要先于它定义之类的。很显然,TypeScript 并无很好的去分析依赖关系。官方解决方案是手工加入 /// <reference path="..." /> 来申明依赖。因此源码中会发现很多这样的文件头。


传送门

相关文章
相关标签/搜索