爲什麼選擇 TypeScript

前言

相信經常關注前端技術的同學對 TypeScript 應該不陌生,或多或少看過一些關於 TypeScript 的文章。

各種技術論壇上也有不少關於 TypeScript 和 JavaScript 的討論,大多數人對 TypeScript 都有着不錯評價,但也有不少人覺得它沒有存在的必要。

事實上,TypeScript 作爲前端編程語言界的當紅炸子雞,配合代碼編輯器界的當紅炸子雞 VS Code 一起食用,能夠讓人擁有極佳的用餐哦不編碼體驗。

許多過去一直使用 JavaScript 的同學,在使用 TypeScript 之後,都覺得再也回不去了。微軟的這一套組合拳,打得多少人大喊真香!(真香定律雖遲但到)

它不是好不好用的問題,它真的是那種,那種很少見的那種…

魯迅先生曾說過:人生苦短,我用 TS 。

回到正題

作爲假前端的我,使用 TypeScript 進行開發也有近兩年的時間了,也希望和各位分享一下我的看法。

所以在本篇文章我將以一名 Cocos Creator 開發者的角度,來對 TypeScript 做一波客觀分析(強行安利),希望對各位有所幫助。


大綱

1. 什麼是 TypeScript

2. TypeScript 存在的意義

3. TypeScript 帶來了什麼改變

4. TypeScript 有什麼特性

5. Cocos Creator 中 TS 和 JS 在使用上的區別

6. 如何創建 Cocos Creator TS 項目

7. 原有的 JS 項目如何使用 TS


正文

什麼是 TypeScript

TypeScript 是一種由微軟開發並開源的跨平臺編程語言,最初開發 TypeScript 的目的是爲了更好地開發大型項目,其作者爲大名鼎鼎的 C# 之父 Anders Hejlsberg

在 TypeScript 中文主頁中對於 TypeScript 的定義是“JavaScript 的超集”, TypeScript 支持JavaScript 的所有語法語義和最新的 ECMAScript 特性,並且額外添加了很多特性

通過 TypeScript 編譯器(tsc),TypeScript 代碼可以被編譯成純淨、簡潔的 JavaScript 代碼

主頁中對 TypeScript 的介紹:


TypeScript 存在的意義

生產力工具

TypeScript 雖爲大型項目而生,但是不代表它不適用於中小型項目,只是項目越大收益越明顯。

TypeScript 彌補了 JavaScript 的許多不足,同時保留了 JavaScript 的靈活性,大大提高了項目的開發效率以及可維護性。

TypeScript 的誕生不是爲了取代 JavaScript ,而是讓 JavaScript 變得更好。

所以 TypeScript 對於開發者來說,不僅僅是一門編程語言,更是生產力工具。

前途大好

TypeScript 的良好口碑以及日漸龐大的生態,早就已經證明了它自己。

許多優秀的開源項目例如前端三大框架 AngularReactVue 均已支持 TypeScript ,Angular2 和 Vue 3.0 都是直接用 TypeScript 開發的

大多數第三方 JavaScript 庫都提供了對 TypeScript 的支持

並且 Node.js 作者近期正式發佈的 Deno 1.0 也是原生支持 TypeScript

可以看到 TypeScript 的未來一片光明…

你幾乎天天用來寫代碼的 VS Code 也是用 TypeScript 編寫的。(用記事本寫代碼的大佬請先收起你的菜刀)

Cocos Creator

而對於 Creator 開發者來說最最最重要的是:

Cocos Creator 引擎開發團隊也建議開發者使用 TypeScript 進行開發。

目前 Creator 3D 只支持使用 TypeScript 進行開發。

我可以大膽的說未來 TypeScript 將會成爲 Cocos Creator 開發的標配!


TypeScript 帶來了什麼改變

既然 TypeScript 爲大型項目而生,那不如就讓我們看看 TypeScript 爲什麼適合大型項目?

在項目的開發中,必定少不了衆多的開發人員,在這個模塊化的時代,一個項目的多個模塊可能均由不同的人來開發,並且每個人都有不同的編碼習慣

在使用 JavaScript 進行開發時,由於沒有類型限制、自動補全和智能提示,就需要開發人員之間的頻繁溝通或者頻繁閱讀文檔(詳細的文檔很關鍵)來保證代碼可以正確執行。

即便如此,開發人員也不能保證每個變量/函數名都一次寫對每個參數都一次傳對

這些溝通和翻閱文檔所花費的時間都在默默降低項目的整體開發效率

而使用 TypeScript 進行開發時,得益於類型系統,在讀取變量或調用函數時,均有自動補全基本杜絕寫錯變量/函數名的情況

類型限制智能提示讓開發人員調用 API 時可以快速得知參數要求,不需要再頻繁閱讀代碼、文檔或詢問模塊開發者。

所有變量、函數和類都可以快速溯源(跳轉到定義),讓 TypeScript 代碼有着較好的可維護性。合理利用註釋甚至可以完全不看文檔,真正做到“註釋即文檔”(文檔還是要有的 : p)。

總之就是開發效率 +++ !


TypeScript 的特性

類型系統

衆所周知 JS 是一門弱類型語言,不到執行時都不能確定變量的類型。編碼時可以隨心所欲反正不報錯,一不小心就寫出八哥( undefined 警告!)。

1. 靜態類型檢查

靜態類型檢查讓 TS 在編輯器中披上強類型語言的“馬甲”,使得開發者在編碼時就可以避免大多數類型錯誤的情況發生,而開發者要做的就只是聲明變量時多寫一個符號和一個單詞

當然你也可以在聲明變量時不指定類型或者使用 any 類型來達到 JS 的動態類型效果,讓 TypeScript 變成 AnyScript ,任性~

let name: string = '陳皮皮';
name = 9527; // 報錯

let age: any = 18;
age = 'eighteen'; // 不報錯

真正做到 早發現,早解決,早下班

2. 原始類型

TS 在支持與 JS 基本相同的原始類型之外,還額外提供了**枚舉(Enum)和元組(Tuple)**的支持。

Cocos Creator 用戶狂喜:再也不需要 cc.Enum 了 ; p

// 枚舉
enum Direction {
    Up = 1,
    Down,
    Left,
    Right
}
let direction: Direction = Direction.Up;

// 元組
let x: [string, number];
x = ['hello', 10]; // 不報錯
x = [10, 'hello']; // 報錯

3. 智能提示

類型系統配合聲明文件(關於聲明文件我們後面再聊)給我們帶來了編輯器中完善的自動補全智能提示,大大增加了開發效率,也再不會因爲拼錯變量名或函數名而導致運行時的錯誤。

我知道 JS 加插件也能實現一定程度的智能提示但是語言自帶它不香嗎 : )

真香警告

修飾符和靜態關鍵字

淚目,是從 C# 那裏幾乎原汁原味搬過來的一套修飾符和關鍵字,主要如以下幾個:

1. 訪問修飾符(public、private 和 protected)

用來限定類成員的可訪問範圍

沒有 internal 和 protect internal

沒有訪問修飾符的封裝莫得靈魂!

class Me {
    public name = '陳皮皮'; // 大家都知道我叫陳皮皮
    private secret = '*******'; // 我的祕密只有我知道
    protected password = '********'; // 我的支付寶密碼會告訴我的後人的
}

let me = new Me();
let a = me.name; // 拿到了我的名字
let b = me.secret; // 報錯,私有的屬性
let c = me.password; // 報錯,受保護的屬性

class Child extends Me {
    constructor() {
        super();
        this.name = '陳XX';
        this.secret // 報錯,無法訪問
        this.password = '888888'; // 可以訪問
    }
}

2. 靜態關鍵字(static)

用於定義全局唯一的靜態變量和靜態函數

在 Creator 的 JS 腳本中是使用 cc.Class 的 statics 屬性來定義靜態成員的,使用體驗一言難盡…

另外在 ES6 中 JS 已經支持靜態函數,在 ES7 中也加入了對靜態屬性的支持。

class Whatever {
    public static origin: string = 'Whatever';
    public static printOrigin() {
        console.log(this.origin);
        console.log(Whatever.origin);
    };
}

console.log(Whatever.origin); // Whatever
Whatever.printOrigin(); // Whatever

3. 抽象關鍵字(abstract)

用來定義抽象類或抽象函數,面向對象編程很重要的一環。

沒對象的可以面向工資編程…

abstract class Animal {
    abstract eat(): void; // 不同動物進食的方式不一樣
}

let animal = new Animal(); // 報錯,法實例化抽象類無

class Dog implements Animal {
    eat() {
        console.log('我喫,汪!');
    }
}

let dog = new Dog();
dog.eat(); // 我喫,汪!

class Cat implements Animal {
    // 報錯了,沒有實現進食的功能
}

4. 只讀關鍵字(readonly)

用來定義只讀的字段,使得字段只能在創建的時候賦值一次

class Human {
    name: string;
    readonly id: number;
    constructor(name: string, id: number) {
        this.name = name;
        this.id = id;
    }
}

let human = new Human('陳皮皮', 666666);
human.name = '陳不皮'; // 名字可以改
human.id = 999999; // 報錯,身份證號碼一旦確定不能更改

接口(Interface)

C# 和 Java 的朋友們讓我看到你們的雙手好嗎

接口用於一系列成員的聲明,但不包含實現,接口支持合併(重複聲明),也可以繼承於另一接口。

下面展示幾個常見的用法:

1. 擴展原始類型

// 擴展 String 類型
interface String {

    /**
     * 翻譯
     */
    translate(): string;

}

// 實現翻譯函數
String.prototype.translate = function () {
    return this; // 不具體寫了,直接返回原字符串吧
};

// 使用
let nickname = '陳皮皮'.translate();

2. 定義類型

interface Human {
    name: string; // 普通屬性,必須有但是可以改
    readonly id: number; // 只讀屬性,一旦確定就不能更改
    hair?: number; // 可選屬性,挺禿然的
}

let ChenPiPi: Human = {
    name: '陳皮皮',
    id: 123456789,
    hair: 9999999999999
}

3. 類實現接口

interface Vehicle {
    wheel: number;
    engine?: string;
    run(): void;
}

class Car implements Vehicle {
    wheel: 4;
    engine: '帝皇引擎';
    run() {
        console.log('小汽車跑得快!')
    }
}

class Bike implements Vehicle {
    wheel: 2;
    run() {
        console.log('小黃車沖沖衝!')
    }
}

類型別名(Type)

這是一個比較常用的特性,作用如其名。

類型別名用來給類型起一個新的名字

類型別名和接口很相似,類型別名可以作用於原始類型,聯合類型,元組以及其它任何你需要手寫的類型,接口支持合併而類型別名不可以。

類型別名同樣也支持擴展,並且可以和接口互相擴展。

// 給原始類型起個小名
type UserName = string;
let userName: UserName = '陳皮';

// 還可以是函數
type GetString = () => string;
let getString: GetString = () => {
    return 'i am string';
}
let result = getString();

// 創建一個新的類型
type Name = {
    realname: string;
    nickname: string;
}
let name: Name = {
    realname: '吳彥祖',
    nickname: '陳皮皮'
}
// 再來一個新的類型
type Age = {
    age: number;
}
// 用上面兩個類型擴展出新的類型
type User = Name & Age;
let user: User = {
    realname: '吳彥祖',
    nickname: '陳皮皮',
    age: 18,
}

聯合類型(Union Types)

使用聯合類型允許你在聲明變量或接收參數時兼容多種類型

個人最喜歡的特性之一,點贊!

1. 表示一個值可以是幾種類型之一

let bye: string | number;
bye = 886; // 不報錯
bye = 'bye'; // 不報錯
bye = false; // 報錯

2. 讓函數接受不同類型的參數,並在函數內部做不同處理

function padLeft(value: string, padding: string | number) {
    if (typeof padding === 'string') {
        return padding + value;
    } else {
        return Array(padding + 1).join('') + value;
    }
}

padLeft('Hello world', 4); // 返回 '    Hello world'
padLeft('Hello', 'I said: '); // 返回 'I said: Hello'

泛型(Generics)

C# 和 Java 的朋友們再次讓我看到你們的雙手好嗎

使用泛型可以讓一個類/函數支持多種類型的數據,使用時可以傳入需要的類型

又是一個非常實用的特性,利用泛型可以大大增加代碼的可重用性,減少重複的工作,點贊!

以下是兩個常用的用法:

1. 泛型函數

// 這是一個清洗物品的函數
function wash<T>(item: T): T {
    // 假裝有清洗的邏輯...
    return item;
}

class Dish { } // 這是盤子
let dish = new Dish(); // 來個盤子
// 盤子洗完還是盤子
// 用尖括號提前告訴它這是盤子
dish = wash<Dish>(dish);

class Car { } // 這是汽車
let car = new Car(); // 買輛汽車
// 汽車洗完還是汽車
// 沒告訴它這是汽車但是它認出來了
car = wash(car);

2. 泛型類

// 盒子
class Box<T>{
    item: T = null;
    put(value: T) {
        this.item = value;
    }
    get() {
        return this.item;
    }
}

let stringBox = new Box<String>(); // 買一個用來裝 String 的盒子
stringBox.put('你好!'); // 存一個 '你好!'
// stringBox.put(666); // 報錯,只能存 String 類型的東西
let string = stringBox.get(); // 拿出來的是 String 類型

裝飾器(Decorator)

這是一個相對比較高級的特性,以 @expression 的形式對類、函數、訪問符、屬性或參數進行額外的聲明

利用裝飾器可以做很多騷操作,感興趣的話可以深入研究下。

對類做預處理

export function color(color: string) {
    return function (target: Function) {
        target.prototype.color = color;
    }
}

@color('white')
class Cloth {
    color: string;
}
let cloth = new Cloth();
console.log(cloth.color); // white

@color('red')
class Car {
    color: string;
}
let car = new Car();
console.log(car.color); // red

Creator 中的 TS 組件中的 ccclass 和 property 就是兩個裝飾器

const { ccclass, property } = cc._decorator;

@ccclass
export default class CPP extends cc.Component {

    @property(cc.Node)
    private abc: cc.Node = null;

}

命名空間(namespace)

命名空間用來定義標識符的可用範圍,主要用於解決重名的問題,對於項目模塊化有很大的幫助。

Cocos Creator 中的 cc 就是一個內置的命名空間。

1. 對相同名字的類和函數進行區分

// pp 命名空間
namespace pp {
    export class Action {
        public static speak() {
            cc.log('我是皮皮!');
        }
    }
}

// dd 命名空間
namespace dd {
    export class Action {
        public static speak() {
            cc.log('我是弟弟!');
        }
    }
}

// 使用
pp.Action.speak(); // 我是皮皮!
dd.Action.speak(); // 我是弟弟!

2. 對接口進行分類

namespace Lobby {
    export interface Request {
        event: string,
        other: object
        // ...
    }
}

namespace Game {
    export interface Request {
        event: string,
        status: string
        // ...
    }
}

// 用於 Lobby 的請求函數
function requestLobby(request: Lobby.Request) {
    // ...
}

// 用於 Game 的請求函數
function requestGame(request: Game.Request) {
    // ...
}

聲明文件(Declaration Files)

聲明文件,即以 d.ts 作爲後綴的代碼文件,用來聲明當前環境中可用的類型。

聲明文件這一特性對於 TypeScript 來說是極其重要的,代碼編輯器中的智能提示等特性都依賴於聲明文件。

可以發現目前大多數第三方 JavaScript 庫都有聲明文件,聲明文件讓這些庫在代碼編輯器中也可以擁有類型檢查智能提示等特性,使用體驗 Max 。

我們甚至可以聲明一些環境中不存在的類型,例如我在《微信小遊戲接入好友排行榜》這篇文章中編寫的 wx.d.ts 文件,使得我在編輯器環境中調用根本不存在的 wx 函數時不會報錯且有智能提示。

一般 Cocos Creator 項目的根目錄下都有一個聲明文件 creator.d.ts ,文件中聲明瞭 Cocos Creator 引擎幾乎所有可用的 API 。所以即使是純 JavaScript 的 Creator 項目,使用 cc 命名空間時也有智能提示。


Creator 中 TS 和 JS 在使用上的區別

聲明組件

在 TypeScript 腳本中 class 的聲明方式 和 ES6 Class 相似,並使用了裝飾器 @ccclass 來將普通 class 聲明成 CCClass :

const { ccclass } = cc._decorator;

@ccclass
export default class Test extends cc.Component {

}

在 JavaScript 腳本中聲明的方式:

cc.Class({
    extends: cc.Component,

});

聲明屬性

在 TypeScript 腳本中需要使用裝飾器 @property 來聲明屬性,基本類型可以不傳參數(參數和使用 JavaScript 時基本一致):

const { ccclass, property } = cc._decorator;

@ccclass
export default class Test extends cc.Component {

    @property
    myNumber: number = 666;

    @property
    myString: string = '666';

    @property
    myBoolean: boolean = true;

    @property(cc.Node)
    myNode: cc.Node = null;

    @property([cc.Node])
    myNodes: cc.Node[] = [];

    @property({
        visible: true,
        displayName: '位置',
        tooltip: '就是一個位置'
    })
    myVec2: cc.Vec2 = new cc.Vec2();

    @property({
        type: cc.Sprite,
        visible() { return this.myBoolean },
        tooltip: '當 myBoolean 爲 true 纔會展示該屬性'
    })
    mySprite: cc.Sprite = null;

    @property
    _getset = 0;
    @property
    get getset() { return this._getset }
    set getset(value) { this._getset = value }

}

在 JavaScript 腳本中需要在 properties 中定義屬性(使用時沒有智能提示,就很難受):

cc.Class({
    extends: cc.Component,

    properties: {
        myNumber: 666,
        myString: '666',
        myBoolean: true,
        myNode: cc.Node,
        myNodes: [cc.Node],
        myVec2: {
            default: new cc.Vec2(),
            visible: true,
            displayName: '位置',
            tooltip: '就是一個位置'
        },
        mySprite: {
            type: cc.Sprite,
            default: null,
            visible() { return this.myBoolean },
            tooltip: '當 myBoolean 爲 true 纔會展示該屬性'
        },
        _getset: 0,
        getset: {
            get() { return this._getset; },
            set(value) { this._getset = value; }
        }
    }

});

導入/導出組件/模塊

在 TypeScript 腳本中使用 ES 模塊的方式來導出或導入組件/模塊:

// A.ts
const { ccclass, property } = cc._decorator;

@ccclass
export default class A extends cc.Component {

    @property
    public nickname: string = 'A';

    public greet() {
        cc.log('A: greet()');
    }

}

// B.ts
import A from "./A";

const { ccclass, property } = cc._decorator;

@ccclass
export default class B extends cc.Component {

    @property(A)
    private a: A = null;

    onLoad() {
        // 訪問實例屬性
        let nickname = this.a.nickname;
        // 調用實例函數
        this.a.greet();
    }

}

在 JavaScript 腳本中默認使用的是 Common JS 模塊的方式

其實 cc.Class 會默認導出,但是 VS Code 識別不了,且使用時也沒有智能提示全靠手打

// A.js
let A = cc.Class({
    extends: cc.Component,

    properties: {
        nickname: 'A'
    },

    greet() {
        cc.log('A: greet()');
    }

});

module.export = A;

// B.js
let A = require('./A');

let B = cc.Class({
    extends: cc.Component,

    properties: {
        a: {
            type: A,
            default: null
        }
    },

    onLoad() {
        // 訪問實例屬性
        let nickname = this.a.nickname;
        // 調用實例函數
        this.a.greet();
    }

});

module.export = B;

靜態變量/函數

在 TypeScript 腳本中直接使用 static 關鍵字聲明靜態變量和函數:

// A.ts
const { ccclass, property } = cc._decorator;

@ccclass
export default class A extends cc.Component {

    public static id: number = 999999;

    public static staticGreet() {
        cc.log('A: staticGreet()');
    }

}

// B.ts
import A from "./A";

const { ccclass, property } = cc._decorator;

@ccclass
export default class B extends cc.Component {

    onLoad() {
        // 訪問靜態屬性
        let id = A.id;
        // 調用靜態函數
        A.staticGreet();
    }

}

在 JavaScript 腳本中使用 statics 屬性來定義靜態變量或函數(使用時沒有智能提示全靠手打):

// A.js
let A = cc.Class({
    extends: cc.Component,

    statics: {
        id: 999999,

        staticGreet() {
            cc.log('A: staticGreet()');
        }
    }

});

module.export = A;

// B.js
let A = require('./A');

let B = cc.Class({
    extends: cc.Component,

    onLoad() {
        // 訪問靜態變量
        let id = A.id;
        // 調用靜態函數
        A.staticGreet();
    }

});

module.export = B;

枚舉

上面也有說到 TypeScript 自帶枚舉類型,所以在 TypeScript 腳本中可以直接用 enum 關鍵字來定義枚舉,而在 JavaScript 腳本中需要用 cc.Enum 來定義枚舉。

// TypeScript 腳本的方式
enum Direction {
    Up = 1,
    Down,
    Left,
    Right
}

// JavaScript 腳本的方式
const Direction = cc.Enum({
    Up = 1,
    Down,
    Left,
    Right
});

如何創建 Creator TS 項目

新建項目時,在項目模板中選擇 Hello TypeScript ,就可以創建一個含有 TypeScript 相關配置和基本組件的項目。


原有的 JS 項目使用 TS

添加配置

想要在原有的 JavaScript Creator 項目中使用 TypeScript ,需要點擊編輯器上方主菜單的 [開發者 -> VS Code 工作流 -> 更新 VS Code 智能提示數據][開發者 -> VS Code 工作流 -> 添加 TypeScript 項目配置] 來給項目添加 creator.d.ts 聲明文件和 tsconfig.json 配置文件。

  • creator.d.ts 是 Cocos Creator 引擎 API 的聲明文件
  • tsconfig.json 是 TypeScript 項目的環境配置文件

混用

在 Creator 項目中添加配置後可以混用 JS 和 TS 腳本,也能享受到 TS 到來的福利。也就是說原有的 JS 腳本可以保留,不影響後續添加新的 TS 腳本。

重構

但是如果想要將項目完全重構爲 TS 項目,要做的就是將原有的 JS 腳本逐個修改爲 TS 腳本,並對腳本內的寫法進行轉換。

對於較爲複雜的項目,對項目代碼進行重構這一行爲可能需要花費較長的時間,如果沒有做好充足的準備,不建議着手進行。

但是一旦完成重構,TS 絕不會讓你失望,必定會給項目開發帶來全方位的提升!


相關資料

TypeScript 官網
https://www.typescriptlang.org

TypeScript 中文網
https://www.tslang.cn

TypeScript 開源代碼倉庫
https://github.com/Microsoft/TypeScript

Cocos Creator TypeScript 使用文檔
https://docs.cocos.com/creator/manual/zh/scripting/typescript.html

TypeScript 入門教程 by xcatliu
https://github.com/xcatliu/typescript-tutorial

ECMAScript 6 入門 by 阮一峯
https://es6.ruanyifeng.com

awesome-typescript by semlinker
https://github.com/semlinker/awesome-typescript


傳送門

微信推文版本

個人博客:菜鳥小棧

開源主頁:陳皮皮

Eazax-CCC 遊戲開發腳手架


更多分享

多平臺通用的屏幕分辨率適配方案

圍繞物體旋轉的方案以及現成的組件

一個全能的挖孔 Shader

一個開源的自動代碼混淆插件

微信小遊戲接入好友排行榜(開放數據域)


公衆號

菜鳥小棧

我是陳皮皮,這是我的個人公衆號,專注但不僅限於遊戲開發、前端和後端技術記錄與分享。

每一篇原創都非常用心,你的關注就是我原創的動力!

Input and output.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章