丁香医生小程序的组件库重构技术选型上采用 TypeScript 进行开发,对开源社区的 Vant-weapp (采用TS开发的小程序组件库)进行了详细的代码阅读,将过程中的一些收获与大家分享。
阅读Vant-weapp项目看到的一些亮点
- 组件统一去走构造函数,可以在其中做公共的处理,扩展微信组件的能力,如计算属性等等
- 很多小程序原生特性的使用
- 工程化
- 文档系统
项目的目录结构
见项目 Readme
抛出一个问题,是否有必要达成一些共识
- 开发目录 src
- 构建脚本目录 build
- 打包目录 dist
- typings
- .gitignore,.editorconfig
- package.json 中一些字段的默认填充
- Readme 中是否需要有目录结构的说明
TypeScript
个人对于 TypeScript 的理解,提供了一些 JavaScript 的语法补充,更多的是紧扣 Type 这个主题,在开发/编译阶段进行类型检查。
开发到目前阶段,组件库应用的特性不是特别的多,大多是 methods
工具函数的入参和出参定义,更多的高级特性应用还有待探索,慢慢给高级特性用起来,总结最佳实践。
有几个遇到的坑
关于接口有两段代码
第一段
interface Person {
name: string;
age: number;
}
let person: Person = {
name: 'haha',
age: 123,
gender: 'man'
}
第二段
function makeFriend(friend: Person) {
console.log(friend);
}
let person1 = {
name: 'haha',
age: 123,
gender: 'man'
}
makeFriend(person1);
小程序原生特性的使用
- WXS
- 组件间关系
- behaviors
- 内置behaviors,wx://form-field
- wx.createSelectorQuery
- NodesRef
- addGlobalClass
- 小程序生命周期
WXS 的应用
WXS(WeiXin Script)是小程序的一套脚本语言,结合 WXML,可以构建出页面的结构。
在每一个组件的 WXML 中都通过标签引入一个工具 wxs,可以在 WXML 中进行一些更为复杂的运算,这个项目中的应用主要是样式的计算,相对于之前很多复杂的三元运算简洁了许多。
截取一段示例代码
<wxs src="../wxs/utils.wxs" module="utils" />
<view class="van-stepper custom-class">
<view
class="minus-class {{ utils.bem('stepper__minus', { disabled: minusDisabled }) }}"
hover-class="van-stepper__minus--hover"
hover-stay-time="70"
bind:tap="onMinus"
/>
再举一个例子,给定一个数组,模板中跑一个循环,在数组中的项目给定高亮样式。
组件的构造函数
每一个组件的构造函数都封装了一层,通过构造函数生成最后的配置,扩展了一些行为,完整的构造函数。
import { basic } from './mixins/basic';
import { observe } from './mixins/observer/index';
import { isObj } from './utils';
function DxdComponent<Data, Props, Watch, Methods, Computed>(
dingOptions: DxdComponentOptions<
Data,
Props,
Watch,
Methods,
Computed,
CombinedComponentInstance<Data, Props, Watch, Methods, Computed>
> = {}
): void {
let options: any = {};
const { relations } = dingOptions
options = { ...dingOptions };
if (relations) {
options.relations = {};
Object.keys(relations).forEach(key => {
options.relations[`../${key}/index`] = relations[key]
})
}
// add default externalClasses
options.externalClasses = options.externalClasses || [];
options.externalClasses.push('external-class');
// add default externalStyle
if (isObj(options.properties)) {
options.properties.externalStyle = String;
}
// add default behaviors
options.behaviors = options.behaviors || [];
options.behaviors.push(basic);
// map field to form-field behavior
if (dingOptions.field) {
options.behaviors.push('wx://form-field');
}
// add default options
options.options = {
multipleSlots: true,
addGlobalClass: true
};
observe(dingOptions, options);
Component(options);
}
export { DxdComponent };
对构造函数包装了一层之后,可以将很多公共的部分写在其中,将原生的不友好的语法变的更加简单,是一个非常好的实践。
难懂的构造函数的类型约束
function VantComponent<Data, Props, Watch, Methods, Computed>(
vantOptions: VantComponentOptions<
Data,
Props,
Watch,
Methods,
Computed,
CombinedComponentInstance<Data, Props, Watch, Methods, Computed>
> = {}
): void {
// ... 函数体
}
type CombinedComponentInstance<
Data,
Props,
Watch,
Methods,
Computed
> = Methods &
LooseObject &
Weapp.Component &
Weapp.FormField &
ComponentInstance & {
data: Data & LooseObject & RecordToAny<Props> & RecordToReturn<Computed>;
};
type VantComponentOptions<
Data,
Props,
Watch,
Methods,
Computed,
Instance
> = {
data?: Data;
field?: boolean;
mixins?: Mixins;
props?: Props & ThisType<Instance>;
watch?: Watch & ThisType<Instance>;
computed?: Computed & ThisType<Instance>;
relation?: Relation<Instance>;
relations?: Relations<Instance>;
classes?: ExternalClasses;
methods?: Methods & ThisType<Instance>;
// lifetimes
beforeCreate?: (this: Instance) => void;
created?: (this: Instance) => void;
mounted?: (this: Instance) => void;
destroyed?: (this: Instance) => void;
};
第一眼看到的时候直接就懵了,其中有这样几个概念需要重点理解一下:
这一段类型是啥意思呢,从调用的地方慢慢来看
传入了几个泛型变量,vantOptions
参数的类型是 VantComponentOptions
,又将泛型变量传入了 VantComponentOptions
类型中的 CombinedComponentInstance
类型,看一下 CombinedComponentInstance
的类型定义。
type CombinedComponentInstance<
Data,
Props,
Watch,
Methods,
Computed
> = Methods &
LooseObject &
Weapp.Component &
Weapp.FormField &
ComponentInstance & {
data: Data & LooseObject & RecordToAny<Props> & RecordToReturn<Computed>;
};
这里先忽略引用到的其他类型,通过泛型传递,在这里就将传入 components 构造函数参数的类型给确定出来。即 VantComponentOptions
定义中的 Instance
的类型。
type VantComponentOptions<
Data,
Props,
Watch,
Methods,
Computed,
Instance
> = {
data?: Data;
field?: boolean;
mixins?: Mixins;
// 在这里通过 ThisType 来约束对象字面量中的 this 类型
props?: Props & ThisType<Instance>;
watch?: Watch & ThisType<Instance>;
computed?: Computed & ThisType<Instance>;
relation?: Relation<Instance>;
relations?: Relations<Instance>;
classes?: ExternalClasses;
methods?: Methods & ThisType<Instance>;
// typescript 提供一个显示的 this 参数,一个假的参数,出现在参数列表的最前面,这里通过 this 来约束函数中的 this 类型
beforeCreate?: (this: Instance) => void;
created?: (this: Instance) => void;
mounted?: (this: Instance) => void;
destroyed?: (this: Instance) => void;