虽然 Creator 很早就支持了 TypeScript,直到 3.0 我们才正式废弃了对 JavaScript 的支持。但随着这次 3.0 的全面升级,很多第一次使用 TypeScript 的开发者仍然遇到了不少阻碍。
为了让大家的升级更加平滑,本文将对这次调整做一次简要回顾,并且解答开发者在语言升级的过程中容易遇到的一些问题。
我们在 TS 上的铺垫
我们对 TypeScript 的支持由来已久,最早可追溯到 17 年 5 月发布的 v1.5.0,此后我们一直在听取 TS 开发者的反馈,逐步完善 TS 的支持。
在 18 年的时候,3D 项目组开始用 TS 重写整个 3D 引擎和编辑器,验证了 TS 处理超大规模项目时的能力,让我们对 TS 有了更大的信心和第一手的使用经验。
到了 19 年,我们在 Cocos Creator 3D 正式发布时,第一次正式废弃了对 JavaScript 的支持,而后持续听取开发者们对这一改变的反馈。
要采用哪一门语言做为引擎的开发语言,不是一个能轻易做出的决定,对开发者、对生态都影响巨大。
Cocos Creator 当初选择 JS 时,已经经历过一次饱受非议,这次如果不是有前面几年在 TS 上的铺垫,我们也没有勇气在 3.0 做出这个关键调整。
为什么要转向 TS
在 Cocos Creator 14 年刚立项之初,由于熟悉的语法,TypeScript 成为了我们入门 JavaScript 的良好桥梁。但由于当时的 TS 刚刚正式发布,生态仍未成熟,因此我们只能先使用最原生的 JavaScript。
经过了这么多年的发展,今天 TS 已经成为一门非常成熟的语言,有丰富的生态和群众基础,可以胜任任何类型的游戏开发需要!
从我这几年跟研发团队的接触来看,大多数团队都逃不过 TS 的真香定律,网上也有大量对 TS 的安利文章,那么我这边就不赘述 TS 的优势。我分享下从我们的角度,我们是怎么想的。
-
我们始终相信,良好的编程语言是项目成功的基石。诚然,任何语言和框架都只是工具,优秀的程序员不论用什么工具都能把项目做好。不过良好的语言更能降低项目整体风险,让开发人员心情愉悦,提升协作效率,从长远来看更能降低整个团队的研发成本!
-
我们始终相信,Cocos Creator 不仅仅是一个小游戏引擎,它必定要能支撑大型重度游戏的开发。JavaScript 或许能满足高手自己做一些小项目的需要,或许能快速做点小东西玩玩,但是达不到大型项目的工业标准。
-
我们始终相信,JavaScript 做为弱类型语言,迟早会成为 Cocos Creator 项目的优化瓶颈,只有强类型语言才有机会彻底提升整个项目的性能表现。所以我们持续在探索 JavaScript 编译为原生语言的可能性,这离不开 TypeScript 的支持。今天已经有 AssemblyScript 这样成熟的项目支持将 TypeScript 方言编译为 WebAssembly,事实上同类项目还有很多。只有从生态、社区的角度统一大家的开发语言,将来我们才有机会为大家送上这份大礼。
-
我们始终相信,语言的割裂会带来生态的割裂。今天在 Cocos Creator 社区已经拥有了一帮 TypeScript 的簇拥,很多优质的教程、插件、帖子都使用 TypeScript 发布。如果官方文档、官方案例都使用一门语言编写,将不利于另一门语言的学习。如果社区长期拥有两门语言,更不利于大家复用前人的工作成果。JavaScript 开发者无法适应 TypeScript 项目的维护,TypeScript 开发者在复用原有的 JavaScript 组件时也会遇到阻碍。
以下内容由我们的引擎质监局长收集自论坛反馈
常见的 TypeScript 认知误区
误区:Cocos Creator 仅支持 TypeScript,不支持 JavaScript
TypeScript 是 JavaScript 的超集并且 TypeScript 紧紧依赖 JavaScript。Cocos Creator 3.0 仍支持 TypeScript 和 JavaScript 并用。
然而,Cocos Creator 鼓励用户使用 TypeScript 以获得更好的开发体验,提高开发质量,因此在编辑器中 仅支持创建 TypeScript 脚本。
如果你确定一定要使用 JavaScript,以其他方式(资源管理器、访达等)创建 JavaScript 文件仍然是允许的。
误区:import/export
是 TypeScript 专有的,我无法在 JavaScript 中使用 require
import
/export
是 JavaScript 规范 ECMAScript 2015 引入的对模块支持的语法,因此 JavaScript 中是可以使用的,并不是 TypeScript 专有。
另一方面,require/exports/module
是 CommonJS 模块系统中的变量。它们不属于 JavaScript 标准。
尽管,在 3.0 中提供了对 CommonJS 模块的有限支持。
将 JavaScript 代码迁移为 TypeScript
上述有提到 JavaScript 可以直接使用。但只要了解 TypeScript 的用法,将 JavaScript 代码迁移为 TypeScript 也是一件轻松的事。
最简单的迁移:改后缀名
可以直接将 .js
文件重命名为 .ts
完成最简单的迁移——只要 JavaScript 在运行时没问题,那么如此改为 TypeScript 也一定是没问题的。
改名后,在 IDE 中会有一大堆报错,这是因为缺少类型信息。如果你暂时不想解决这些类型问题,大可以在整个文件的头部加一句注释:
// @ts-nocheck
来跳过对该文件的类型检查。
在代码中补充类型信息
对于比较简单的 JavaScript 代码,只要稍加补充一些类型信息就可以。例如:
function fn(a, b, c) {
// ...
}
为 fn
指定参数类型就 OK:
function fn(a: string, b: number, c: boolean) {
// ...
}
TypeScript 会自动根据函数体内的代码推断出函数的返回值。你当然也可以显式指定:
function fn(a: string, b: number, c: boolean): string {
// ...
}
在代码外部声明类型信息
有些时候,JavaScript 代码不是我们自己写的,而是由第三方提供的库代码。这时候我们可以在不编辑它的情况下为它补充类型信息。
例如,有一个第三方的 JavaScript 文件 foo.js:
module.exports = function foo (a, b, c) {
// ...
}
我们可以在同目录下创建一个同名但扩展名为 .d.ts
的 foo.d.ts
:
// 声明 foo 的签名即可,函数体不需要
function foo(a: string, b: number, c: boolean): string;
export default foo;
严格模式:“null” 问题
从 JavaScript 转入 TypeScript 的同学可能被一些“类型问题”所困扰。
看这样一个问题:
class C {
material: Material = null;
}
这段代码在 IDE 中会报错,报错源头是属性 material
的声明。
有一种情况是,material
属性仅在初始化时是空值,但是后续任何时候访问都是有值的。例如,给该属性附加 @property
装饰器时,就可以在编辑器中编辑该字段,拖拖拉拉之后由 Creator 帮我们赋值该字段。
那我们如何向 TypeScript 传达这种信息呢?
我们来分析一下报错的原因,material: Material
将 material
字段声明为 Material
类型,这个意思就是在任何时候拿到 material
它都是 Material
类型。
初始化式 = null
告诉它将 material
初始化为 空值 null
,与上条说法违背。
这便是,TypeScript 的严格类型检查。它要求你将类型对上号。TypeScript 编辑器是默认开启该选项的,Creator 也不例外。
既然知道了出错原因,那么我们就可以有以下几种思路去解决。
- 正确描述它的类型为:既可能是
Material
,也可能是null
- 向 TypeScript 类型系统表达:我只是初始化为 null,后续使用时候其实都是
Material
- 关闭 TypeScript 对空值的检查。
可空类型
我们可以将 material
的类型描述为既可能是 Material
,也可能是 null
:
material: Material | null = null;
这样初始化为 null
就顺理成章了。
然而,这样的缺陷是当你后续访问 material
时,TypeScript 要求你处理空值的情况。例如:
console.log(this.material.name);
TypeScript 类型系统会提示你:this.material
可能是空值 null
,无法访问 null
的属性。
但是,只要 TypeScript 在此处知道它一定是不为空的,它就会收起这条错误:
if (this.material) {
console.log(this.material.name);
}
因为你做了判断,当运行到 if
语句块内的时候,this.material
一定是不为空的,因此可以访问 name
属性。
表达式非空断言
每次使用的时候都要用 if
语句判断一下实在有些繁琐,况且 if
语句在运行时是会去执行的。
如果你一定能确保“在运行到这里的时候 this.material 一定非空”,那么我们可以用TypeScript 非空断言语法来表达:
console.log(this.material!.name);
感叹号 !
称作 非空断言操作符,它断言前面的表达式是非空的。
注意此感叹号是 TypeScript 特有的,仅为类型目的;编译后,会直接移除。
我们还可在初始化时就作此断言:
material: Material = null!; // 相当于将 `null` 强制转换为了 `Material` 类型
还一个经常用于非空断言的地方是 Node.getComponent()
,此方法返回的是可能为空的组件。如果能确保组件一定存在,则可以通过非空断言来避免 if 判断。
显式赋值断言
在标准的 JavaScript 语法里,是可以不给初始化式的,这样的字段将被初始化为 undefined
:
class C {
@property(Material)
material; // undefined
}
在 TypeScript 里也允许这么做,不过你得提示一下 TypeScript,以让它跳过这里的类型检查:
material!: Material;
属性名后的感叹号称为 显式赋值断言(Definite Assignment Assertion),它告诉 TypeScript 此字段在别处初始化。
OK,这里你将得到一个虽然声明为 Material
,但是初始化为 undefined
的字段。
禁用空值检查
当然了,如果你是非常不喜欢 TypeScript 的类型系统,你更喜欢所有事靠自己确保,那么你可以关闭严格类型检测。在 <项目目录>/tsconfig.json 里,加上选项:
"extends": "./temp/tsconfig.cocos.json",
"compilerOptions": {
"strictNullChecks": false // 关闭它
}
需要提醒的是,我们并不鼓励这种做法,因为严格空值检查能够减少 JS 代码运行时的一些低级报错。放一张最近我们收到的用户反馈做为旁证:
总结
以上就是此次跟大家介绍的全部内容。感谢大家在论坛里将工作中遇到的问题如实反馈,才让我们有机会整理成文。
如果大家仍有 TypeScript 使用上的问题,可以在本帖回复,也欢迎大家将本文转发给有需要的人!