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="..." /> 來申明依賴。因此源碼中會發現很多這樣的文件頭。


傳送門

相關文章
相關標籤/搜索