Cocos Creator 3.0 TypeScript 使用答疑
3.0 合并了原有 2D 和 3D 两套产品的所有功能,不仅延续了 Cocos 在 2D 品类上轻量高效的优势,并且为 3D 重度游戏提供高效的开发体验。
我们很高兴看到 3.0 在发布后引发了热烈的讨论,也收到许多同学的反馈,这当中也包括了许多不一样的声音,有人觉得我们想太多,也有人觉得我们想太少,等等等等。
我们也知道大家都希望我们第一个版本就非常完美、没有 bug,我们呢当然是希望大家不要有这种希望,开玩笑开玩笑哈哈哈。
其实社区的帖子我们都有看到,始终非常感谢大家一直以来的默默支持,和耐心等待。这次我们也集结引擎组所有功能开发的一线大佬,开辟《Cocos Creator 3.0 技术专栏》,希望能帮助大家丝滑升级,减少开发阻碍。
我们也会在 3.0 的基础上继续深耕,用心踏踏实实完善产品体验,普惠所有开发者。希望大家可以感受到我们真诚的态度和敬业的精神,放心地给 3.0 一个机会。
本篇为专栏第一篇文章,作者 Leslie+Jare,希望能给大家带来帮助,欢迎阅读。
虽然 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:
module.exports = function foo (a, b, c) {
// ...
}
严格模式:“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 代码运行时的一些低级报错。放一张最近我们收到的用户反馈做为旁证:
总结
以上就是此次跟大家介绍的全部内容。感谢大家在论坛里将遇到的实际问题与我们反馈交流,才让我们一路以来越来越好。
过去一年我们助力开发者创作出了许多精彩游戏,未来我们仍会在 3.0 的基础上大力深耕,突破引擎技术的天花板,普惠广大开发者。大家可以放心给 3.0 一个机会,多多使用 3.0 用于实际项目制作。
后续本专栏将不定期更新,为大家带来包括但不限于以下内容:
《Cocos Creator 3.0 里如何玩转 npm 海量资源》by 放空
《深入Cocos Creator 3.0 的插件扩展系统》by sijie
《Cocos Creator 3.0 的 3D 物理讲解》by Jayce Lai
《Cocos Creator 3.0 的资源系统》by Santy Wang
......
让我们在此先隆重感谢各位作者,并以此道德绑架他们,希望他们尽快交稿哈哈哈。
如果大家有关于 3.0 其他想看的内容或是使用上的问题,或是其他想说的,欢迎在评论处留言告诉我们。
我们将在本周五(3月5日)为评论区点赞最高的同学送出 Cocos 周边大礼包,并随机抽出 5 位童靴随机送出周边,希望能听到更多大家的声音,也欢迎大家将本文转给有需要的同学。
最后,再次感谢大家一路陪我们到这里,一起去到更远的地方吧。