阅读 710

为什么选择 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 识别不了,所以一般都会用 module.export 导出。且使用时也没有智能提示全靠手打):

// 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 脚本中使用 static 属性来定义静态变量或函数(使用时没有智能提示全靠手打):

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

    static: {
        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;
复制代码

枚举

上面也有说到 TS 自带枚举类型,所以在 TS 脚本中可以直接 enum 来定义枚举,而在 JS 脚本中需要用 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.