Typescript接入小程序 - 用更标准的TS来写小程序

11,375 阅读4分钟

TS与小程序的深度结合

前几篇都是讲如何用TS来写代码,那么如何将TS应用于小程序呢?或者让TS更好的应用的小程序?

小程序代码主要是组件页面两个模块,那么接下来我们就从组件页面开始封装。

前置知识

如何将官方的小程序代码写法,变个样?

官方的推荐的小程序代码的写法是这样的

// Component
Component({
    properties: {},
    data: {},
    attached() {},
    detached() {},
    methods: {},
    // more...
})


// Page

Page({
    data: {},
    onLoad() {},
    onShow() {},
    onHide() {},
    onUnload() {},
    // more...
})

他们的本质上是PageComponent接受一个对象作为参数。

那么我们可以得出他们的另一种ES6的写法

// Component

class Demo {
	properties = {};

	data = {};

	attached() {}

	detached() {}

	methods = {};
}

Component(new Demo())

为什么可以这样写?

我们先看看编babel译成ES5的代码

"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
var Demo = (function () {
    function Demo() {
        this.properties = {};
        this.data = {};
        Demo.prototype.attached = function() {};
        Demo.prototype.detached = function() {};
        this.methods = {};
    }
    return Demo;
}());
Component(new Demo());

我们用ES6写法最后传给Component任然是一个满足官方要求有的属性方法的对象

用TS写小程序怎么写

官方的写法

type TSizeValue = 'large' | 'medium' | '' | 'small';

type TSizeProp = {
  type: StringConstructor,
  value: TSizeValue
}
Component({
  properties: {
    // 它规定了开发者 只能给size 传入 'large' | 'medium' | '' | 'small'
    // 这四选一属性,这样在开发的时候就能起到了约束作用
    size: <TSizeProp>{
      type: String,
      value: ''
    }
  },
  data: {
    value: ''
  },
  methods: {
    handleClick(e: any):void {}
  }
})

优点:能检测到官方定义的类型声明文件。

缺点:这样写,其实很难体现TS的语言特性,很憋足。我这么用接口?怎么体现class 修饰符?

期望写法是怎样的?

// component
class DemoComp implements Component {
    properties = {};
    data = {};
    methods = {};
    // more...
}

// page
class DemoPage extends BasePage {
  public onLoad() {}

  public onShow() {}

  // more...
}

优点:像正常写TS文件一样去写小程序,能充分利用TS语言特性

缺点:...

那么实现这一效果,需要有三个条件

  • 如何让class XXX具有官方定义的类型声明
  • 如何去掉Component(new Demo())这种代码,让TS代码更纯粹、更优雅
  • 如何兼容低版本SDK
如何让class XXX具有官方定义的类型声明

这无疑是最核心的条件

要实现这个功能,我们需要先来看官方定义文件,这里用Page封装距离,Component原理封装类似

// lib.wx.page.d.ts

declare namespace WechatMiniprogram {
    namespace Page {
        type Instance<
            TData extends DataOption,
            TCustom extends CustomOption
        > = OptionalInterface<ILifetime> &
            InstanceProperties &
            InstanceMethods<TData> &
            Data<TData> &
            TCustom
        type Options<
            TData extends DataOption,
            TCustom extends CustomOption
        > = (TCustom & Partial<Data<TData>> & Partial<ILifetime>) &
            ThisType<Instance<TData, TCustom>>
        type TrivialInstance = Instance<IAnyObject, IAnyObject>
        interface Constructor {
            <TData extends DataOption, TCustom extends CustomOption>(
                options: Options<TData, TCustom>,
            ): void
        }
        
        ....
}

declare const Page: WechatMiniprogram.Page.Constructor

我们的实际写TS代码是Page({}),那么不难发现,我们实际是实现的是

interface Constructor {
    <TData extends DataOption, TCustom extends CustomOption>(
        options: Options<TData, TCustom>,
    ): void
}

我们传入的对象实际的类型定义是options: Options<TData, TCustom>

那么我们只需要实现Options 这个声明了。先看其定义

type Options<
    TData extends DataOption,
    TCustom extends CustomOption
> = (TCustom & Partial<Data<TData>> & Partial<ILifetime>) &
    ThisType<Instance<TData, TCustom>>

我们可以轻易写出让class 具有Options的什么

// BasePage.ts

class BasePage implements WechatMiniprogram.Page.Options<
    WechatMiniprogram.Page.DataOption,
    WechatMiniprogram.Page.CustomOption
> {

}

这样BasePage就具备了Options定义的属性和方法声明。

但是当在业务里面使用的时候

// 引入 BasePage.ts

class DemoPage extends BasePage {
    public onLoad() {
        this.setData({}) // 类型“DemoPage”上不存在属性“setData”
    }
}

如何让BasePage具有setData的方法声明?

// 看setData 在哪里定义的

type InstanceMethods<D extends DataOption> = Component.InstanceMethods<D>

那么我们只需要实现这个类型就行了,那么问题来了?

如何让class BasePage既具有type Options的类型声明又具有type InstanceMethods的类型声明

答案就是:声明合并

// BasePage.ts

interface BasePage extends WechatMiniprogram.Page.InstanceMethods<WechatMiniprogram.Page.DataOption> {

}

class BasePage implements WechatMiniprogram.Page.Options<
    WechatMiniprogram.Page.DataOption,
    WechatMiniprogram.Page.CustomOption
> {

}

这样BasePage 达到刚刚的目的了。

如何去掉Component(new Demo())这种代码,让TS代码更纯粹、更优雅

通过构建过程中,给TS文件插入类似Page(new DemoPage())这样的代码,这里不再赘述了

如何兼容低版本SDK

为什么还有这个问题?

运行上面这段代码,在SDK 2.7.2以下的版本会报如下错误。

// error 1
Uncaught TypeError: Cannot assign to read only property 'constructor' of object '#<V>'
    at G (VM63 WAService.js:1)
    at Object.t (VM63 WAService.js:1)
    at mt (VM63 WAService.js:1)
    at Rt (VM63 WAService.js:1)
    at index.ts:8
    at require (VM63 WAService.js:1)
    at <anonymous>:11:7
    at HTMLScriptElement.scriptLoaded (appservice?t=1569326156124:1147)
    at HTMLScriptElement.script.onload (appservice?t=1569326156124:1159)
    
// error 2
Page is not constructed because it is not found.

怎么解决?

interface BasePage extends WechatMiniprogram.Page.InstanceMethods<WechatMiniprogram.Page.DataOption> {

}

class BasePage implements WechatMiniprogram.Page.Options<WechatMiniprogram.Page.DataOption, WechatMiniprogram.Page.CustomOption> {
  constructor() {
    // @ts-ignore
    delete this.__proto__.constructor
  }
}

最终效果

import BasePage from '@lib/Page';


export default class OrderConfirm extends BasePage {

    private options: WashOrderConfirm.TPageOptions = {
        orderId: ''
    };
    
    public data = {
        test: ''
    };
    
    public onLoad(options: WashOrderConfirm.TPageOptions) {
        this.options = options;
        this.testMethods();
    }
    
    private testMethods () {
        this.setData({ test: 'demo' })
    }
}