评论

为什么选择使用 TypeScript ?

万字长文!以一个 Cocos Creator 开发者的角度来详细谈谈为什么选择使用 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. Creator 中 TS 和 JS 在使用上的区别
6. 如何创建 Creator TS 项目
7. 原有的 JS 项目如何使用 TS



正文

1. 什么是 TypeScript

TypeScript 是一种由微软开发并开源的跨平台编程语言,最初开发 TypeScript 的目的是为了更好地开发大型项目,其作者为大名鼎鼎的 C# 之父 Anders Hejlsberg


在 TypeScript 中文主页中对于 TypeScript 的定义是“JavaScript 的超集”, TypeScript 支持JavaScript 的所有语法语义和最新的 ECMAScript 特性,并且额外添加了很多特性


通过 TypeScript 编译器(tsc),TypeScript 代码可以被编译成纯净、简洁的 JavaScript 代码


主页中对 TypeScript 的介绍:



2. 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 编写的。(用记事本写代码的大佬请先收起你的菜刀)


而对于 Creator 开发者来说最最最重要的是:

Cocos Creator 引擎开发团队也建议开发者使用 TypeScript 进行开发。

目前 Creator 3D 只支持使用 TypeScript 进行开发。

我可以大胆的说未来 TypeScript 将会成为 Cocos Creator 开发的标配!


3. TypeScript 带来了什么改变


既然 TypeScript 为大型项目而生,那不如就让我们看看 TypeScript 为什么适合大型项目?


在项目的开发中,必定少不了众多的开发人员,在这个模块化的时代,一个项目的多个模块可能均由不同的人来开发,并且每个人都有不同的编码习惯


在使用 JavaScript 进行开发时,由于没有类型限制、自动补全和智能提示,就需要开发人员之间的频繁沟通或者频繁阅读文档(详细的文档很关键)来保证代码可以正确执行。

即便如此,开发人员也不能保证每个变量/函数名都一次写对每个参数都一次传对

这些沟通和翻阅文档所花费的时间都在默默降低项目的整体开发效率


而使用 TypeScript 进行开发时,得益于类型系统,在读取变量或调用函数时,均有自动补全基本杜绝写错变量/函数名的情况

类型限制智能提示让开发人员调用 API 时可以快速得知参数要求,不需要再频繁阅读代码、文档或询问模块开发者。

所有变量、函数和类都可以快速溯源(跳转到定义),让 TypeScript 代码有着较好的可维护性。合理利用注释甚至可以完全不看文档,真正做到“注释即文档”。(文档还是要有的)

总之就是开发效率 MAX !


4. 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)的支持。(小声BB:再也不需要 cc.Enum 了)

// 枚举
enum Direction {
    Up = 1,
    Down,
    Left,
    Right
}
let direction: Direction = Direction.Up;
​
// 元组
let x: [string, number];
x = ['hello', 10]; // 不报错
x = [10, 'hello']; // 报错


★ 另外类型系统配合声明文件(关于声明文件我们后面再聊)给我们带来了编辑器中完善的自动补全智能提示,大大增加了开发效率,也再不会因为拼错变量名或函数名而导致运行时的错误。(我知道 JS 加插件也能实现一定程度的智能提示但是语言自带它不香吗?)


修饰符和静态关键字

泪目,是从 C# 那里几乎原汁原味搬过来的一套修饰符和关键字,主要如以下几个:


1. 访问修饰符public private protected

用来限定类成员的可访问范围

人觉得没有访问修饰符的封装莫得灵魂的!
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 的 static 属性来定义静态成员的,个人觉得使用体验极差)。

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; // 报错,身份证号码一旦确定不能更改


接口

( C# 和 Java 的朋友们让我看到你们的双手好吗)

接口(interface)用于一系列成员的声明,但不包含实现,接口支持合并(重复声明),也可以继承于另一接口。下面给大家展示几个常见的用法:

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 和接口(interface)很相似,type 可以作用于原始类型,联合类型,元组以及其它任何你需要手写的类型,interface 支持合并而 type 不可以。

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'


泛型

( C# 和 Java 的朋友们再次让我看到你们的双手好吗)

使用泛型可以让一个/函数**支持多种类型的数据,使用时可以传入需要的类型**。

又是一个非常实用的特性,利用泛型可以大大增加代码的可重用性,*减少重复的工作*,点赞!

以下是两个常用的用法:

1. 泛型函数

// 这是一个清洗物品的函数
function wash(item: T): T {
    // 假装有清洗的逻辑...
    return item;
}
​
class Dish { } // 这是盘子
let dish = new Dish(); // 来个盘子
// 盘子洗完还是盘子
// 用尖括号提前告诉它这是盘子
dish = wash(dish);
​
class Car { } // 这是汽车
let car = new Car(); // 买辆汽车
// 汽车洗完还是汽车
// 没告诉它这是汽车但是它认出来了
car = wash(car);


2. 泛型类

// 盒子
class Box{
    item: T = null;
    put(value: T) {
        this.item = value;
    }
    get() {
        return this.item;
    }
}
​
let stringBox = new Box(); // 买一个用来装 String 的盒子
stringBox.put('你好!'); // 存一个 '你好!'
// stringBox.put(666); // 报错,只能存 String 类型的东西
let string = stringBox.get(); // 拿出来的是 String 类型


装饰器

这是一个相对比较高级的特性,以 @expression 的形式对类、函数、访问符、属性或参数进行额外的声明

利用装饰器可以做很多骚操作,感兴趣的话可以深入研究下。

1. 对类做预处理

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


2. 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 命名空间时也有智能提示。



5. 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
});



6. 如何创建 Creator TS 项目

新建项目时,在项目模板中选择 Hello TypeScript ,就可以创建一个含有 TypeScript 相关配置和基本组件的项目。



7. 原有的 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.

最后一次编辑于  2020-05-20  
点赞 1
收藏
评论
登录 后发表内容