点击蓝字👆 关注Agilean,获取一手干货
直播预告:Adapt 系列直播又双叒叕来啦!最新一期我们将围绕「版本分支与环境」进行深入探讨,欢迎大家来直播间和主播互动哟~
点击下方右上角红色按钮「预约」👇
以下为正文
背景
由于在页面交互行为等层面的优越性,许多研发部门的前端团队会采用JavaScript这一门编程语言。然而,JavaScript是一种动态类型语言,这意味着数据类型错误只会在运行时被发现。运行时类型检查本身并不是缺点:它提供了更大的灵活性,但是项目和团队越大,代码的数量和复杂度越来越高,整个项目的可维护性、鲁棒性(稳健性)会越来越差,进而影响到整个团队的开发效率和质量。
因此,越来越多的前端团队开始考虑替换方案,迁移到TypeScript便是一个出路。
TypeScript是一个由 Microsoft 开发的开源编程语言,它是 JavaScript 的超集,提供了强类型和其他高级功能,能够显著提高代码可读性和可维护性。借助 TypeScript ,开发人员可以更轻松地识别和预防潜在的类型错误,从而减少代码中的bug数量,并更快速地进行开发和重构。
本文,我们将分享知微前端团队的 TypeScript 迁移经验,细数迁移的前因后果与实施注意点。希望能给正在(或者准备)迁移的伙伴们提供一些实实在在的帮助。
成也 JavaScript ,败也 JavaScript
知微前端团队最初是基于 JavaScript 进行开发的。
初期,JavaScript灵活性可以帮助我们快速构建项目,但是随着项目的复杂度的不断增加,这种模式下暴露出来的问题也越来越多,集中体现在以下几个方面:
开发效率低,线上 bug 多
JavaScript 不会对变量类型进行检查,意味着当存在问题时就无法像其他静态类型语言一样在编译期就抛出异常,只能在运行时才能被发现,这不仅需要开发人员在自测阶段耗费大量的时间,还容易让 bug 逃逸到测试阶段甚至是线上。
可维护性差
JavaScript 变量的类型可以随时更改,这使得代码难以预测和理解;全局变量可能会被其他部分的代码意外修改,从而导致意料之外的影响,这种情况很难被发现和调试,对代码的可维护性造成了很大压力。
旧代码不敢碰
每次不得不修改到旧代码,都只能依靠人力进行验证,但人的精力是有极限的。
IDE 自动补全差
由于 JavaScript 是一门动态语言,其类型在运行时才被确定,因此,在开发过程中,IDE难以准确地确定变量和函数的类型和作用域,这导致代码自动补全的准确性随之下降,甚至无法自动补全。
后浪推前浪,TypeScript “入主”
彼时正好 TypeScirpt 在社区比较火,它作为 JavaScript 的一个超集,扩展了 JavaScript 的语法,增加了静态类型检查、接口、泛型等特性,并且可以被编译成普通的 JavaScript 代码。
这几乎完美契合我们的需求:静态类型检查和兼容 JavaScript ,因此我们决定迁移到 TypeScript 。
1. 步步为营
迁移进行时
在迁移 JavaScript 代码到 TypeScript 时,我们可以采用不同的策略,具体取决于团队的特定需求和现有代码的质量。以下是一些常见的迁移策略:
渐进式迁移
这是最常见的迁移策略之一,会涉及 TypeScript 代码与 JavaScript 代码一起使用的过渡期,以便逐步迁移到 TypeScript 。
在这种策略下,我们可以通过在新的 TypeScript 模块中编写新代码,逐步替换旧代码,并使用 TypeScript 的工具和插件来检查和改进代码,实现渐进式迁移。
重构策略
如果代码规模比较小,或者我们需要在 TypeScript 中完全重写代码,那么整体重构可能是一个更好的选择。
在这种情况下,我们需要重新审查我们的代码并将其重写为 TypeScript ,这可能需要更多的时间和精力。但是,如果能成功地重构代码,我们将能够更好地利用 TypeScript 的优势。
基于成本考虑,我们采用了渐进式迁移的策略来将 JavaScript 代码迁移到 TypeScript 。
这种策略的好处在于,它可以让我们在不破坏现有代码的情况下,逐步替换旧代码,并确保新的 TypeScript 代码符合 TypeScript 的语法规则和最佳实践。
具体来说,我们经历了以下几个阶段:
1
配置支持 TypeScript 和 JavaScript 共存
因为 TypeScript 可以通过 tsconfig.json 配置允许 JavaScript 和检查的严格程度,我们可以在项目保持旧 JavaScript 代码的同时引入 TypeScript ,并将 TypeScript 检查的严格程度调整到最低,以免项目无法编译通过,这样可以使得迁移过程更加平滑和稳定,并且可以在迁移过程中避免一些不必要的问题。
2
新代码要求使用 TypeScript 编写
我们要求团队成员新的代码一律使用 TypeScript 编写,但由于当前的 TypeScript 类型检查程度开的是最低的,所以无法通过 tsc 这样的静态类型检测来保证全员遵守 TypeScript 最佳实践,需要额外的办法来确保该机制的运行,比如Code Review,避免将 TypeScript 用成 AnyScript 。
3
旧代码的迁移
在需求的迭代过程中将涉及到的 JavaScript 逐步迁移到 TypeScript ,或者是在版本的迭代过程中穿插一些技术改造需求。
随着新代码的增加和旧代码的迁移,TypeScript 覆盖率到达一定程度后,可以将 TypeScript 的编译器配置逐步调整进行更严格的类型约束,最终实现 TypeScript 覆盖率达到100%。
2.金无足赤
迁移后的利与弊
由于我们以前都是使用 JavaScript 开发,刚刚引入 TypeScript 后有不少需要适应的地方:
弊端
需求开发所需要的时间增加了。
因为 TypeScript 是一种静态类型语言,开发人员需要花费额外的精力来定义变量和函数的类型,并确保代码的正确性和可用性。这可能会导致开发过程中的一些延迟和额外的努力。
对于没有静态类型编程经验的开发人员来说,使用 TypeScript 可能会有些不习惯。
这是因为 TypeScript 要求我们对代码中的变量和函数进行类型定义,并在编写代码时就要考虑类型的一致性和正确性。这可能需要一些额外的学习和适应时间。
对于没有提供 TypeScript 声明的第三方库,需要自己编写类型声明文件。
因为浏览器仅支持 JavaScript,需要将 TypeScript 转译为 JavaScript ,所以打包相比以前多了一步将 TypeScript 转译为 JavaScript ,具体耗时取决于项目的大小,小型项目可能编译变慢的问题不明显,但中大型项目则影响会比较大。
电脑需求面临更高的性能挑战。
比如 VSCode 需要开启 ts-server 实时运算校验类型和提示,消耗资源过多可能会导致 VSCode 变卡,进而降低开发效率(有理由换性能更强大的电脑了)。
尽管存在以上问题,但整体而言 TypeScript 仍然是一个值得投资的选择:
优势
相较 JavaScript,更强的 IDE 提示和自动补全功能。比如前端开发看后端某个接口返回值,一般需要去 Network 或接口文档看才能知道返回数据结构,而正确用了 TypeScript 后,IDE 会提醒接口返回值的类型,这节省了不少时间。
支持静态类型检测。使用 TypeScript 我们可以使用其提供的 tsc 命令进行整个项目的类型检测,直接检测出当前项目存在哪些类型错误,而不需要在运行时去人工检测代码是否会引起异常以及担心是否有遗漏的适配,大大减少了开发人员的心智负担和自测耗时,同时,还降低了 bug 逃逸率,减少了线上 bug,简直是农耕时代和工业时代的区别。
及早发现问题。借助 IDE 对 TypeScript 的支持,在编码过程中就能够发现部分问题,可以极大提升开发效率(项目规模越大,差别会愈加显著)。
代码可读性与可维护性更佳。使用 TypeScript 可以帮助我们更好地组织和结构化代码,并提供更明确的类型定义和命名约定。这使得代码更易于阅读和理解,也更容易维护。
代码重构能力更强。使用 TypeScript 可以帮助我们更快速地进行代码重构,因为我们可以在编译时就捕获到许多潜在的问题。这使得代码重构更加安全、可靠,也更容易进行。
3.披巾斩棘
使用 TypeScript 遇到的问题及解决方法
对于一些第三方库无法进行较好的类型约束
比如,在 redux-saga 中,我们无法直接对 action 和对应 saga 进行类型约束关联。
TypeScript |
TypeScript |
TypeScript |
在上述示例代码中,fetchUserAction 和 fetchUserSaga 这两个类型理论上是有联系的,该 Action 的返回值即为Saga的入参,虽然我们使用了 TypeScript 定义了fetchUserAction 和 fetchUserSaga 的参数类型,但这两个类型间的联系是割裂的,只要两者中任一处的类型被调整了,另一处都无法通过 TypeScript 静态类型检查得到反馈。
针对这类场景,我们可以借助 TypeScript 的类型编程来实现手动绑定类型:
TypeScript |
TypeScript |
TypeScript |
通过上述代码,我们可以实现当 fetchUserAction 的返回值类型发生变更时,能够通过 TypeScript 的静态类型检测得到类型错误提示。而且上述其实大部分都是模板代码,因此我们可以通过编写一些工具通过入参来自动生成这些模板代码。
TypeScript 还在不断完善
尽管 TypeScript 在过去几年中已经获得了广泛的认可和采用,但它仍是一个相对年轻的语言,因此仍然存在一些特性的支持不够完善的情况。
例如,在 TypeScript 4.1 之前,如果从一个 Record 类型的对象中获取一个属性值,那么这个属性值的类型将被假定不会为 null 或 undefined 。这意味着如果你尝试获取一个不存在的属性或者一个属性值可能为空的情况,TypeScript 将不会在编译时给出任何警告,这可能导致在运行时出现错误。
TypeScript |
为此,我们通过TypeScript自定义出一个KVMap类型:
TypeScript |
借助 KVMap 我们实现了安全地进行索引取值,但是KVMap这个类型仍有局限性,在一些内置方法,比如 Object.entires、Object.values 等方法中,其返回值的类型又多了冗余的 undefined,整体还不是很优雅(小编吐槽:你们啥时候优雅过了🙄️)。
直到 TypeScript 4.1 引入了 noUncheckedIndexedAccess选项。通过启用这个选项,开发者可以告诉TypeScript认为从索引中获取的值具有可能为空的类型,完美地解决了Record 和 KVMap 的局限性。
虽然 TypeScript 仍然处于成长阶段,并且可能存在某些特性的支持不够完善的情况,但它的发展速度非常迅速, TypeScript 的开发团队正在不断地推出新的版本和更新,以支持更多的语言特性和更好地解决已知的问题。
4. 里应外合
TypeScript生态内闭环,生态外守护
尽管 TypeScript 提供了静态类型检查,但是在实际开发中,我们经常需要处理从外部接收的数据,例如从API接口、数据库等场景中接收的数据。这些数据的类型可能与我们在代码中定义的类型不一致,因此我们需要进行运行时类型校验,以确保数据的准确性。
基于前后端互不信任原则,以下代码只能是声明接口会返回一个User类型的数据,但无法保证API返回的数据是严格遵守类型声明。
TypeScript |
如果直接使用以上代码,就有可能让非预期数据进入到前端业务处理代码,因为非法操作而导致前端页面崩溃,更可怕的是经过前端将错误数据又传到后端数据库进行存储,会进一步导致其他关联业务出现错误,一旦出现这种问题就非常难以排查,因为出错的地方并不是根源所在。
基于上述,我们引入了运行时类型校验的库 zod,用来对外部系统的输入进行校验。只有在校验通过后该数据才能进入到前端业务代码内部,这样既避免外部系统的错误输入不会导致前端页面崩溃,又确保前端项目代码内部的类型一定是正确的。
TypeScript |
小结
以上是知微前端团队的 TypeScript 迁移历程。
虽然 TypeScript 相比 JavaScript 有很多好处,但在选择使用 TypeScript 还是 JavaScript 时,我们需要根据具体情况进行权衡,因地制宜。
总的来说,对于小型项目或者对项目要求较低的情况下,JavaScript 是一个不错的选择。因为它具有较高的灵活性和易用性,而且学习成本相对较低。而对于中大型项目, TypeScript 可以提供更好的代码维护性和可读性,提高团队协作的效率。
本文作者|欧阳城
知微前端团队负责人
直播别忘了约,因为目前不考虑回放,所以感兴趣的伙伴记得准时来喔!
分享👇 收藏👇 点赞👇在看👇