基于 TypeScript 的 Weex 优化实践

一、背景

Weex 作为一种成熟的跨平台程序框架被运用到许多产品中,有赞也不例外。有赞零售移动端团队从 2018 年就开始使用 Weex 构建页面,据不完全统计,有赞零售移动端有超过300个页面使用到了 Weex 开发!显然,这是一个巨大的开发工程,同时我们也发现基于 JavaScript 的 Weex 开发给我们带来了诸多痛点:

  • 缺少类型约束,编程时代码提示全凭记忆,要拓展新功能也束手束脚。
  • 手误写错某个变量名,只能在联调、测试阶段才能发现。
  • 面对复杂业务逻辑,代码层面可控性、扩展性较差。
  • 总会遇到 xxx is undefined 的空指针问题。

这些问题我们都在 TypeScript 找到了答案。

二、什么是TypeScript

TypeScript 是微软开源的编程语言,它建立在 JavaScript 的基础上,是 JavaScript 的超集,可以编译成 JavaScript。它有以下特点:

  1. 始于JavaScript,归于JavaScript
    TypeScript 从今天数以百万计的 JavaScript 开发者所熟悉的语法和语义中拓生而来,所使用的是通用的 JavaScript 代码,包括流行的 JavaScript 库,从 JavaScript 代码中调用 TypeScript 代码轻而易举。
    TypeScript 可以编译出纯净、 简洁的 JavaScript 代码,并能运行在任何支持 ES3 及以上的 JavaScript 引擎中。

  2. 强大的工具构建
    类型允许 JavaScript 开发者在开发 JavaScript 应用程序时使用高效的开发工具和常用操作,比如静态检查和代码重构。 类型是可选的,类型推断让一些类型的注释与你的代码的静态验证有很大的不同。类型让你能自主定义软件组件之间的接口和洞察现有 JavaScript 库的行为。

  3. 进阶的JavaScript
    TypeScript 提供最新的和不断发展的 JavaScript 特性,包括那些来自 ES2015 和未来提案中的特性,比如异步功能和装饰器,以帮助建立健壮的组件。

三、为什么要使用TypeScript

1. 降低维护成本,提升健壮性、稳定性

1)代码即文档,好的接口、函数定义可直接代替文档,代码可读性更高。
2)静态类型检查,提早发现问题代码。

2. 提高开发效率

1)对代码重构和补全提示友好。
2)多人协作降低沟通成本,不再需要频繁阅读文档或细究实现细节。
3)类型可选,让你在不编写额外代码的情况下获得很多功能。
4)有很多先进的高级特性可以使用。

3. 成熟度高

1)编辑器或 IDE 集成度高。
2)社区庞大,周边工具丰富。
3)最受欢迎的编程语言排行榜中已跃升至第 10 名,话题度高。
4)多个团队全面使用 TypeScript 重构代码(Vue、React 、Angular),甚至连 Facebook 自家的产品(Jest、Yarn等等)都在从 Flow 向 TypeScript 迁移。

最受欢迎编程语言排行榜

4. 接入成本低

1)几乎没有接入成本,对当前工程改造小。
2)可以和 JavaScript 混合开发、编译成 JavaScript 在各端运行。
3)支持多种工具链。

5. 学习成本低

几乎没有学习成本,移动端各自开发的语言本身就有类型系统,并且 Swift、kotlin 也有可选类型,语法也和 TypeScript 类似。

四、如何使用 TypeScript 进行 Weex 开发

随着 Vue2.x 对 TypeScript 的支持,Weex 也能快速接入 TypeScript。同时 Vue3.0 将使用 TypeScript 重写,重写后的 Vue3.0更能发挥 TypeScript 的特点。

1.接入TypeScript

虽然市面上关于 Weex 支持 TypeScript 的资料比较少,但关于 Vue 如何接入 TypeScript 的文章铺天盖地,这里做个简单总结:

  • 添加 TypeScript 依赖,根据所需升级相关依赖或者有影响的包(当使用第三方库时,我们需要引用它的声明文件,才能获得对应的代码补全、接口提示等功能)。
  • ts-loader可选,如果之前的项目工程对Babel依赖比较重,可以保留 babel-loader(Babel>=7)。Babel 已和TypeScript 官方展开了合作,解决了部分之前不能被正常编译的问题。或者通过使用两个编译器,搭配 ts-loaderbabel-loader 来接入 TypeScript。
  • 添加 tsconfig.json,并加入相关你需要的自定义配置。
  • 官方对 ESLint 做了支持,提供了解析 TypeScript 代码的编译器,可以把语法树转成 ESLint 所期望对 ESTree,使用 @typescript-eslint 即可。
  • 添加必要的声明文件,Weex 目前还没有官方的声明文件,大家可按需添加。

2.声明文件

Weex 官方目前没有对 TypeScript 提供优秀的支持,需要自行添加声明文件。

比如:const platform = weex.config.env.platform

在 TypeScript 中,编译器并不知道 Weex 是什么东西。这时我们需要对其声明

声明文件必需以 .d.ts 为后缀。一般来说,TypeScript 会解析项目中所有的 *.ts 文件,当然也包含以 .d.ts 结尾的文件。

例:weex.d.ts

declare interface IWeexGlobal {  
    config: {
        platform: 'Android' | 'iOS' | 'Web'
    }
}
declare const weex: IWeexGlobal  

Typescript 默认不能识别 .vue 文件,导致在引用时,会提示加载错误。所以需要自己新建一个 .d.ts 声明文件文件添加以下内容。这是为了告诉 Typescript 以 .vue 结尾的导入的任何东西都与 Vue 构造函数本身具有相同的形状。注意引用 vue 组件时需要补全 .vue

例:vue.d.ts

declare module '*.vue' {  
    import Vue from 'vue'
    export default Vue
}

关于声明文件详细内容,具体可参考官方文档

3.类组件

要让 TypeScript 正确推断 Vue 组件选项中的类型,需要使用类组件。在Vue 2.x 中,通常使用基于 Vue Class Component 装饰器来用使用类组件。

Vue Class Component 对 Vue 组件进行了一层封装,让 Vue 组件语法在结合了 TypeScript 语法之后更加扁平化,代码可读性更高。

使用组类组件有以下差异:

  • @Component 修饰符注明了此类为一个 Vue 组件
  • 初始数据可以直接声明为实例的 property
  • 计算属性可以直接使用 getter / setter
  • 组件方法也可以直接声明为实例的方法
  • 所有 Vue 生命周期也可以直接声明为实例方法,但是你不能在实例本身上调用它们。 声明自定义方法时,应避免使用这些保留名称
  • 其他接口描述对象可以传递给装饰器函数或者 Vue.extend

类组件和接口描述组件区别

其他接口描述对象在类组件的使用:

其他接口描述对象

TypeScript 的类组件和 JavaScript 的接口描述组件导出有些差异:

  • 类组件导出的是 Vue 类
  • 接口描述组件导出的是ComponentOptions接口

所以在入口文件对Vue进行初始化上也会有些区别:

入口文件区别

4.装饰器

TypeScript 支持装饰器这一特性,Javascript 里的装饰器目前处在建议征集的第二阶段。若要使用装饰器特性,需要在 tsconfig.json 里启用 experimentalDecorators 编译器选项。装饰器的好处如下:

1)使语法更加扁平化。
2)对业务代码无侵入。
3)解耦业务逻辑、辅助功能逻辑。

除了上节提到的 @ComponentVue Property DecoratorVuex Class 提供了更多的装饰器用于使用。装饰器可以用于修饰类、方法和属性等。

Vue Property Decorator

  • @Prop
  • @PropSync
  • @Model
  • @Watch
  • @Provide
  • @Inject
  • @ProvideReactive
  • @InjectReactive
  • @Emit
  • @Ref
  • @Component (由 vue-class-component 提供)

对常用对 @Prop@Watch 举个例:

使用装饰器区别

关于其他装饰器如何使用,具体参考官方文档

Vuex Class

  • @State
  • @Getter
  • @Action
  • @Mutation

关于如何使用,具体参考官方文档

开发工具

1)Visual Studio CodeWeb Storm 都能做到开箱即用,不需要装配额外的插件。
2)对 ZWeex ToolKit 扩展能力,目前已经支持了创建 TypeScript 的页面

五、落地 TypeScript 提升系统稳定性

我们来对之前遇到的问题做个拆解,看看 TypeScript 是如何帮我们解决痛点。

减少Bug

类型错误

TypeScript 的类型保护、联合类型、类型推导等特性,可以避免发生低级类型错误问题。
比如在开发中约定函数的参数是 number 数字类型,如果使用时不慎使用了 string 类型的参数,那么 IDE 会有 error 警告并会在编译时报错。 IDE提示错误 编译时报错

空指针

TypeScript 会进行严格非空检查可以帮助我们避免空指针问题。

比如函数的参数定义是允许出现空指针的情况,那么在使用这些不安全的参数时,IDE 和编译器都会提醒你这块儿地方注意了,如果没有处理边界会给予提示。 IDE提示错误 编译时报错

添加了判断空指针进行处理异常边界之后,可以通过编译。

正常情况

原生 module 类型约束

有赞零售使用有近 20 个原生 module,在之前开发过程中因为没有类型约束出现过不少写错 module/方法/参数名、使用错参数类型的情况。 使用 TypeScript 的类型声明可以解决这些问题。

举个例子,有以下几个原生 module,我们对其类型声明

declare interface IWeexNativeModules {  
    foo: {
        fun(a: number, b?: string): void
    }

    foo1: {
        oops(): void
    }

    foo2: {
        oops(): void
    }
}

使用时,IDE会有代码补全提示。如果写错 IDE 和编译器同样报错提示。 代码补全

调用方法和参数时也会有类型约束。

通过使用 TypeScript 有效的避免了类型问题,减少 Bug 量。一篇伦敦大学和微软研究院联合署名的论文中提到:

通过对 Github 上开源项目的公开 Bug 统计发现:15% 的 Bug 都可以通过 TypeScript 来规避。

增强架构设计

TypeScript 比 JavaScript 多了接口、抽象类、范型、访问权限等,可以方便的落地架构设计。

面向接口(协议)编程在移动端应用是非常广泛的,使用 TypeScript 之后也可以进行一些架构设计。

之前我们在使用 Weex 进行开发时,往往会把所有逻辑代码往组件内部塞,使得组件后续维护起来非常麻烦。我们引入了和原生一样的规范:增加Model、Service 层,通过工具自动生成相应目录结构,在开发中得到了非常好的约束。

规范目录结构

效果

我们在 Q2 完成了 TypeScript 的迁移,开发效率显著提升、系统稳定性明显提高。

在对供应链单据页模版化的项目中,使用 TypeScript 进行了大范围的重构。我们发现联调期间的沟通显著减少,不需要频繁查阅接口文档,代码可读性更高了,节省了很多 debug 成本。在测试环节仅出现个位数的 Bug,发布线上之后也没问题发生。

六、平滑迁移 TypeScript

迁移工程不需要一蹴而就,你可以先用 JSDoc 注释现有的 JavaScript,然后迁移几个文件交由 TypeScript 检查,随着时间的推移,当你的代码库准备好了之后,代码库迁移到 TypeScript 自然就水到渠成了。

参考链接

欢迎关注我们的公众号