likes
comments
collection
share

Typescript 在电商团队中的实践总结

作者站长头像
站长
· 阅读数 10

TypeScript 在电商团队中的实践总结

TS的特性

JavaScript:

  • 解释型脚本语⾔,⽆需额外编译,便可在浏览器等环境中解释执⾏。

TypeScript:

  • ⾯向对象的编程语⾔,增加了静态类型、接⼝、泛型、类型注解等。
  • 通过静态类型检测,更容易编写健壮性强的代码,更适合⼤型项⽬开发。
  • 浏览器、Node等环境⽆法直接运⾏TypeScript,需编译成JavaScript后运⾏。

优点:

  • 丰富的类型系统:基本类型、接口、类、函数、泛型、枚举......
  • 强大的类型推导能力:支持类型推导,命名空间和模块,装饰器等特性
  • 完善的生态支持:tsc/bable编译器、IDE、eslint、@types

前后端通信之痛

背景:

我之前所在的部门,一个化妆品电商品牌前端团队,N个前端,M个品牌,2N个后端,总共几百人,几乎每天都在不停的开发、联调中,电商业务需求多、迭代块、细节多,一款品牌电商经过历史迭代、多⼈多团队维护,存在代码结构混乱、逻辑耦合极⾼,单⼀⽂件动辄上千⾏,代码可读性、可维护性均较差;基于JavaScript设计的数据结构、数据传递等⾮常复杂,导致线上⼩问题层出不穷,排查修复成本⾼;另外还有是由于每个研发同事的水平、代码风格、做事风格的不同,也难以保持API和字段单词的规范和统一,更别说所有开发人员保证实时更新API文档了。

⽬标:

  • ①.项目太大,在长期的迭代中,代码风格不同,组件、函数升级,参数不兼容
  • ②.前后端接口字段不一致等
  • ③.接口文档与代码不一致等

基于以上一些项目维护中的痛点,决定想要实现通过对项⽬代码的基础能⼒、业务逻辑⼩步快跑式的拆解,降低项⽬代码的复杂度,并形成团队内⼀致的⼯程规范、编码规范等,提升项⽬代码的可读性、可维护性。

通过TypeScript等相关技术栈引⼊,梳理并重构业务代码数据结构、数据传递等逻辑,通过静态类型校验,增加编译时排雷的概率,降低运⾏时出错的⻛险。

维持项⽬技术新度,偿还历史项⽬的技术债务,提升团队同学的技术热情与技术能⼒。

TS在欧莱雅电商业务中的迁移实践

迁移原则

Typescript 在电商团队中的实践总结

实践⽅案

Monorepo (pnpm + TypeScript) + esbuild + Changesets

⽅案设计说明:

以小程序为例,考虑到电商务的代码体量及复杂性,比如兰蔻、YSL、娇兰等电商小程序至少100+页面,妄图一个小程序一个主包吞下整个电商全流程不太现实。所以,我们采取的⽅案是按照功能属性拆分代码并渐进式改造升级。第⼀步,我们对项⽬中"基础能⼒"部分拆分;第⼆步,我们对项⽬中"业务逻辑"与"视图渲染"拆分。

基础能⼒部分,独⽴功能模块众多,因此采⽤Monorepo模式整合。出于构建速度的考量,对TypeScript的Dev环境与Prod环境构建使⽤Vite/webpack + esbuild (团队种老的电商多租户平台还是在用Vue2 + elementui,新电商后台已全面改用vite + Vue3/React)。基础能⼒部分,功能相对独⽴、业务属性弱,在渐进式改造升级的过程中以依赖包的形式集成⼊原⼯程,所以使⽤轻量化、社区活跃度⾼的Changesets来迭代维护依赖包更新。

实践⽅案

在toB的PC端项目方面,⼯程结构:

 .changeset                 -- 基础库版本管理配置
 dist                       -- 基础库production产物
 packages
    - loreal-sign           -- 商城登录基础库
    - loreal-ec             -- EC电商通用基础库
    - loreal-axios          -- 二次封装ec请求库
    - loreal-trace          -- 电商埋点SDK及其工具
    - loreal-utils          -- EC电商工具链
 pnpm-workspace.yaml        -- 
 tsconfig.json              -- dev模式 TypeScritp配置文件
 tsconfig.type-check.json   -- tsc模式 TypeScript配置文件,用于prod模式构建时做并行类型检查
 vite.config.ts             -- vite dev模式配置文件
 vite.prod-config.ts        -- vite prod模式配置文件

在toC的小程序端项目方面,⼯程结构:

  miniprogram
    - api                   -- API治理中心
    - assets                -- 静态资源
    - components            -- loreal电商组件
    - config                -- 与APPID有关的配置信息
    - packages              -- 分包
    - pages                 -- 主包
    - plugins               -- 插件
    - utils                 -- 工具函数,包括wxs
  model
    - api                   -- api接口的types文件、插件的types
    index.d.ts
  node_modules
  typings
    - types                 -- 微信自身api的一些types
    index.d.ts
- package.json
- project.config.json
- project.private.config.json
- tsconfig.json

TS迁移过程中的踩坑记录:

1. 对于toB的项目,Vite对库模式开发⽀持不友好

踩坑:Vite新⽣事物,虽说迭代挺快,但不⾜也挺明显。如,Vite对多⻚应⽤构建配置友好, 但对Monorepo库模式配置能⼒不⾜,需要编程式(Vite -> build())解决构建需求。

2. esbuild⽆类型检查能⼒及声明⽂件⽣成能⼒

踩坑:选择Vite很⼤⼀部分因素是它使⽤esbuild做构建,且esbuild内置了TypeScript编译能⼒, 但esbuild不提供类型检查与声明⽂件⽣成。所以,最终的⽅案是,ESBuild构建同时使⽤TSC编译器并⾏的完成类型检查与声明⽂件⽣成。

3. TSConfig配置⽂件⾃定义问题

踩坑:仅Prod模式就需要2个TS配置⽂件。TSC命令⽐较⽅便指定,但Vite暴露的build函数(执⾏esbuild构建)基本⽆说明⽂档。撸Vite源码找⾃定义配置⽅式,再结合esbuild官⽅⽂档

TS代码迁移技巧

1.不使用TS提供的语法糖(除非已经纳入了ECMAScript标准)

enum、重载、 public, privaete, protected, readonly修饰符、方法装饰器,访问器装饰器,属性装饰器等

2.定义类型时尽量使用交叉类型(不使用extends),Utility定义Types

交叉类型和Mixins有一点区别:交叉类型只是一个类型声明,用于类型约束;Mixins会给类增加成员,new对象时,对象会包含增加的成员属性。 同时TS在全局内置了很多Utility Types,可以极大的提高我们开发效率。

3.公共类型放在src/types或者types下,方便以后统一处理

在日常开发中可能会经常用到webpack的路径别名,比如: import xxx from '@/path/to/name',如果编辑器不做任何配置的话,编译器不会给任何路径提示,更不会给你语法提示。这里有个小技巧,基于 tsconfig.json 的 baseUrl和paths这两个字段,配置好这两个字段后,.ts文件里不但有了路径提示,还会跟踪到该路径进行语法提示。

如果把 tsconfig.json 重命名成jsconfig.json,.js文件里也可以享受到路径别名提示和语法提示。

4.需要时才定义类型,尽量使用类型推断,非复用(简单)类型使用字面量定义

在 ts 中,代码实现中的 typeof 关键词能够帮助 ts 判断出变量的基本类型:

function fn (x: string | number) {
  if (typeof x === 'string') { // x is string
    return x.length
  } else { // x is number
    // .....
  }
}

instanceof 关键词能够帮助 ts 判断出构造函数的类型;在条件判断中,ts 会自动对 null 和 undefined 进行类型保护,如果我们已经知道的参数不为空,可以使用 ! 来手动标记.

function fn2 (x?: string) {
  return x!.length
}

5.暂时不能解决的使用TsFixme & @ts-ignore

在开发过程中,我们都会不时地写一些 // TODO: 和 // FIXME: 注释。有时我们这样做是因为我们知道代码可以做得更好,但暂时不确定如何做,有时由于 deadline 而没有时间编写最佳解决方案,而有时我们只是想着手处理更紧急的事情,这时我们只需在代码中标识一个 // TODO: or // FIXME 提示自己以便在将来某一天再处理。

/**
 * 手动获取用户在天猫旗舰店的会员等级和积分
 * @returns {any | *} 返回会员等级和积分; FIXME: 此处类型较为复杂,先用 any 代替,有空再补
 */

TS迁移流程:

1.先培训

因为一些常用的JS写法,在TS里面需要稍微变通一下才能使用,比如我们常用的promise.all这种方法,在TypeScript中,当你这么用Promise.all时,会遇到类型检查错误的问题。

Promise.all( Promise<XX>[] )

因为lib.es6.d.ts中,对Promise.all这么定义

all<TAll>(values: Iterable<TAll | PromiseLike<TAll>>): Promise<TAll[]>;

所以,对上述案例,TAll被自动识别为了 Promise;而实际上,TAll应该是XX。 该定义文件的PromiseLike跟Promise似乎没什么关系,所以没能自动识别。 所以使用时,如果Promise.all传入的是一个数组,那么建议的用法是强制制定类型,如下。

Promise.all<XX>( Promise<XX>[] )

由比如 解决ts-node中使用symlinks时引用node_modules报错的问题 preserveSymlinks 在 TypeScripts中默认为true 而在 NodeJS 中默认为false

ts-node --preserve-symlinks index.ts

2.项目种常用类型补全,建立标准,服务于全体应用

前后端共享代码和类型定义,全程代码提示和类型报错

3.小范围试验,确保转换过程简单、安全和自动化

先在欧莱雅、兰蔻、薇姿项目中重构改造,在一些重要电商里先行试水,随后在整个电商事业群里推广。改造的范围包括但不限于

①core utils 迁移

②选取核心文件进行迁移

4.总结试验结果并优化

为了减轻这种风险,我们需要一个规范的流程来转换文件,其不会引入回归,也不会诱导工程师去做多余的事情。这个流程还要能快速执行。

我们确定了一个分为两部分的流程:首先自动转换 CoffeeScript 文件,然后立即手动添加基本类型注解和与 linter 相关的更改。关键在于抵制(不管是什么方式)重构代码的诱惑。这样一来,转换工作就成为了简单、遵循安全规则的机械活动,不会影响运行时行为。

5.大范围推广

①新页面新文件、工具函数必须使用ts,老文件修改超过40%需用ts重写

②使用snippet模板

③code review

收益&总结

收益:开发体验必须明显改善

要让整个团队都参与进来,必须让开发人员体会到,他们在编写 TypeScript 时会更有效率。如果团队只是将迁移看作是从一种语法换成另一种差不多的语法,他们永远都不会产生认同感。如果迁移之后他们的日常工作效率并没有提升,那么就算工程师们往往更喜欢编写类型化代码,也敌不过旧习惯的巨大惯性。

将工具链和配置作为优先事项来对待。大多数开发人员使用的编辑器就是那么几种而已,因此我们创建了可以直接使用的编辑器配置,添加了调试配置,从而可以轻松设置断点和单步执行代码。

最后,我们整理了一套取得共识的 linting 规则,这些规则使我们能够在整个组织中以统一的样式编写代码,并让开发人员对迁移行动更加满意。

当团队开始看到这些转换工作的成果时,整个项目也就得到了认可,前进动力也会更足了。当我们的工程师开始将类型化数据访问视为必不可少的工具后,他们就能更好地意识到,代码库的其它部分也会平稳地转换完毕。

总结

⽬前,电商业务渐进式改造升级,第⼀部分“基础能⼒”的拆分进⼊尾声,初步拆分出“账号登录”、“⽹络请求”、“数据埋点”等模块。通过“基础能⼒”模块的拆分,降低了原有代码的冗余度,使相关功能模块的逻辑清晰化、内聚化,可维护性增强;通过TypeScript语⾔的使⽤,使复杂的数据结构类型化、规范化,能更好的应对业务体量的增⻓,提升产品运⾏时的稳定性;同时,新的技术栈及技术⽅案的引⼊,切实提升了团队同学的技术能⼒,营造了团队良好的技术氛围。

全面拥抱Typescript,不仅仅是增加了类型,而是编程方式的转变。

变量 => 类型 => 结构

面向过程 => 面向接口