—▼—
作为假前端的我,使用 TypeScript 进行开发也有一年半的时间了,也希望和各位分享一下我的看法。所以在本篇文章我将以一名 Cocos Creator 开发者的角度,来对 TypeScript 做一波客观分析(强行安利),希望对各位有所帮助。本文由“壹伴编辑器”提供技术支大纲1. 什么是 TypeScript2. TypeScript 存在的意义3. TypeScript 带来了什么改变4. TypeScript 有什么特点5. 如何创建 Creator TS 项目6. 原有的 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 的良好口碑以及日渐庞大的生态,早就已经证明了它自己。许多优秀的开源项目例如前端三大框架 Angular、React 和 Vue 均已支持 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 开发的标配!TypeScript 带来了什么改变
既然 TypeScript 为大型项目而生,那不如就让我们看看 TypeScript 为什么适合大型项目?在项目的开发中,必定少不了众多的开发人员,在这个模块化的时代,一个项目的多个模块可能均由不同的人来开发,并且每个人都有不同的编码习惯。在使用 JavaScript 进行开发时,由于没有类型限制、自动补全和智能提示,就需要开发人员之间的频繁沟通或者频繁阅读文档(详细的文档很关键)来保证代码可以正确执行。即便如此,开发人员也不能保证每个变量/函数名都一次写对,每个参数都一次传对。这些沟通和翻阅文档所花费的时间都在默默降低项目的整体开发效率。而使用 TypeScript 进行开发时,得益于类型系统,在读取变量或调用函数时,均有自动补全,基本杜绝写错变量/函数名的情况。类型限制与智能提示让开发人员调用 API 时可以快速得知参数要求,不需要再频繁阅读代码、文档或询问模块开发者。所有变量、函数和类都可以快速溯源(跳转到定义),让 TypeScript 代码有着较好的可维护性。合理利用注释甚至可以完全不看文档,真正做到“注释即文档”。(文档还是要有的 )总之就是开发效率 MAX !TypeScript 的特点
- 类型系统
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 加插件也能实现一定程度的智能提示但是语言自带它不香吗?)
—▼—
- 修饰符和静态关键字
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(); // Whatever3. 抽象关键字: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 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 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,
}
—▼—
- 联合类型
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'
—▼—
- 泛型
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 类型
—▼—
- 装饰器
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); // red2. 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 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) {
// ...
}
—▼—
- 声明文件
Creator 中 TS 和 JS 在使用上的区别
-
声明组件
@ccclass
export default class Test extends cc.Component {
}在 JavaScript 脚本中声明的方式:cc.Class({
extends: cc.Component,
});
—▼—
- 声明属性
@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; }
}
}
});
—▼—
- 导入/导出组件/模块
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;
—▼—
- 静态变量/函数
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;
—▼—
-
枚举
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 项目的环境配置文件