丁香医生小程序的组件库重构技术选型上采用 TypeScript 进行开发,对开源社区的 Vant-weapp (采用TS开发的小程序组件库)进行了详细的代码阅读,将过程中的一些收获与大家分享。

阅读Vant-weapp项目看到的一些亮点

  • 组件统一去走构造函数,可以在其中做公共的处理,扩展微信组件的能力,如计算属性等等
  • 很多小程序原生特性的使用
  • 工程化
  • 文档系统

项目的目录结构

见项目 Readme

抛出一个问题,是否有必要达成一些共识

  • 开发目录 src
  • 构建脚本目录 build
  • 打包目录 dist
  • typings
  • .gitignore,.editorconfig
  • package.json 中一些字段的默认填充
  • Readme 中是否需要有目录结构的说明

TypeScript

DXDesign 项目搭建 - 图1

个人对于 TypeScript 的理解,提供了一些 JavaScript 的语法补充,更多的是紧扣 Type 这个主题,在开发/编译阶段进行类型检查。

开发到目前阶段,组件库应用的特性不是特别的多,大多是 methods 工具函数的入参和出参定义,更多的高级特性应用还有待探索,慢慢给高级特性用起来,总结最佳实践。

有几个遇到的坑

关于接口有两段代码

第一段

  1. interface Person {
  2. name: string;
  3. age: number;
  4. }
  5. let person: Person = {
  6. name: 'haha',
  7. age: 123,
  8. gender: 'man'
  9. }

第二段

  1. function makeFriend(friend: Person) {
  2. console.log(friend);
  3. }
  4. let person1 = {
  5. name: 'haha',
  6. age: 123,
  7. gender: 'man'
  8. }
  9. makeFriend(person1);

小程序原生特性的使用

WXS 的应用

WXS(WeiXin Script)是小程序的一套脚本语言,结合 WXML,可以构建出页面的结构。

在每一个组件的 WXML 中都通过标签引入一个工具 wxs,可以在 WXML 中进行一些更为复杂的运算,这个项目中的应用主要是样式的计算,相对于之前很多复杂的三元运算简洁了许多。

截取一段示例代码

  1. <wxs src="../wxs/utils.wxs" module="utils" />
  2. <view class="van-stepper custom-class">
  3. <view
  4. class="minus-class {{ utils.bem('stepper__minus', { disabled: minusDisabled }) }}"
  5. hover-class="van-stepper__minus--hover"
  6. hover-stay-time="70"
  7. bind:tap="onMinus"
  8. />

再举一个例子,给定一个数组,模板中跑一个循环,在数组中的项目给定高亮样式。

组件的构造函数

每一个组件的构造函数都封装了一层,通过构造函数生成最后的配置,扩展了一些行为,完整的构造函数

  1. import { basic } from './mixins/basic';
  2. import { observe } from './mixins/observer/index';
  3. import { isObj } from './utils';
  4. function DxdComponent<Data, Props, Watch, Methods, Computed>(
  5. dingOptions: DxdComponentOptions<
  6. Data,
  7. Props,
  8. Watch,
  9. Methods,
  10. Computed,
  11. CombinedComponentInstance<Data, Props, Watch, Methods, Computed>
  12. > = {}
  13. ): void {
  14. let options: any = {};
  15. const { relations } = dingOptions
  16. options = { ...dingOptions };
  17. if (relations) {
  18. options.relations = {};
  19. Object.keys(relations).forEach(key => {
  20. options.relations[`../${key}/index`] = relations[key]
  21. })
  22. }
  23. // add default externalClasses
  24. options.externalClasses = options.externalClasses || [];
  25. options.externalClasses.push('external-class');
  26. // add default externalStyle
  27. if (isObj(options.properties)) {
  28. options.properties.externalStyle = String;
  29. }
  30. // add default behaviors
  31. options.behaviors = options.behaviors || [];
  32. options.behaviors.push(basic);
  33. // map field to form-field behavior
  34. if (dingOptions.field) {
  35. options.behaviors.push('wx://form-field');
  36. }
  37. // add default options
  38. options.options = {
  39. multipleSlots: true,
  40. addGlobalClass: true
  41. };
  42. observe(dingOptions, options);
  43. Component(options);
  44. }
  45. export { DxdComponent };

对构造函数包装了一层之后,可以将很多公共的部分写在其中,将原生的不友好的语法变的更加简单,是一个非常好的实践。

难懂的构造函数的类型约束

component.ts

  1. function VantComponent<Data, Props, Watch, Methods, Computed>(
  2. vantOptions: VantComponentOptions<
  3. Data,
  4. Props,
  5. Watch,
  6. Methods,
  7. Computed,
  8. CombinedComponentInstance<Data, Props, Watch, Methods, Computed>
  9. > = {}
  10. ): void {
  11. // ... 函数体
  12. }

index.d.ts

  1. type CombinedComponentInstance<
  2. Data,
  3. Props,
  4. Watch,
  5. Methods,
  6. Computed
  7. > = Methods &
  8. LooseObject &
  9. Weapp.Component &
  10. Weapp.FormField &
  11. ComponentInstance & {
  12. data: Data & LooseObject & RecordToAny<Props> & RecordToReturn<Computed>;
  13. };
  14. type VantComponentOptions<
  15. Data,
  16. Props,
  17. Watch,
  18. Methods,
  19. Computed,
  20. Instance
  21. > = {
  22. data?: Data;
  23. field?: boolean;
  24. mixins?: Mixins;
  25. props?: Props & ThisType<Instance>;
  26. watch?: Watch & ThisType<Instance>;
  27. computed?: Computed & ThisType<Instance>;
  28. relation?: Relation<Instance>;
  29. relations?: Relations<Instance>;
  30. classes?: ExternalClasses;
  31. methods?: Methods & ThisType<Instance>;
  32. // lifetimes
  33. beforeCreate?: (this: Instance) => void;
  34. created?: (this: Instance) => void;
  35. mounted?: (this: Instance) => void;
  36. destroyed?: (this: Instance) => void;
  37. };

第一眼看到的时候直接就懵了,其中有这样几个概念需要重点理解一下:

DXDesign 项目搭建 - 图2

这一段类型是啥意思呢,从调用的地方慢慢来看

传入了几个泛型变量,vantOptions 参数的类型是 VantComponentOptions,又将泛型变量传入了 VantComponentOptions 类型中的 CombinedComponentInstance 类型,看一下 CombinedComponentInstance 的类型定义。

  1. type CombinedComponentInstance<
  2. Data,
  3. Props,
  4. Watch,
  5. Methods,
  6. Computed
  7. > = Methods &
  8. LooseObject &
  9. Weapp.Component &
  10. Weapp.FormField &
  11. ComponentInstance & {
  12. data: Data & LooseObject & RecordToAny<Props> & RecordToReturn<Computed>;
  13. };

这里先忽略引用到的其他类型,通过泛型传递,在这里就将传入 components 构造函数参数的类型给确定出来。即 VantComponentOptions 定义中的 Instance 的类型。

  1. type VantComponentOptions<
  2. Data,
  3. Props,
  4. Watch,
  5. Methods,
  6. Computed,
  7. Instance
  8. > = {
  9. data?: Data;
  10. field?: boolean;
  11. mixins?: Mixins;
  12. // 在这里通过 ThisType 来约束对象字面量中的 this 类型
  13. props?: Props & ThisType<Instance>;
  14. watch?: Watch & ThisType<Instance>;
  15. computed?: Computed & ThisType<Instance>;
  16. relation?: Relation<Instance>;
  17. relations?: Relations<Instance>;
  18. classes?: ExternalClasses;
  19. methods?: Methods & ThisType<Instance>;
  20. // typescript 提供一个显示的 this 参数,一个假的参数,出现在参数列表的最前面,这里通过 this 来约束函数中的 this 类型
  21. beforeCreate?: (this: Instance) => void;
  22. created?: (this: Instance) => void;
  23. mounted?: (this: Instance) => void;
  24. destroyed?: (this: Instance) => void;