0.17.0-dev.202304160 • Published 3 years ago

ewm v0.17.0-dev.202304160

Weekly downloads
-
License
MIT
Repository
-
Last release
3 years ago

Commitizen friendly

概述

EWM(Enhanced-Wechat-Miniprogram的缩写) 是微信小程序原生开发插件,提供了新的实例构造器(DefineComponent),并加入了新的TS类型系统,让'小'程序拥有"大"能力。gitee地址

  • 增强的实例构造器(DefineComponent)

    新的实例构建函数(DefineComponent)相比于原生构造器(Page/Component),字段逻辑更清晰,功能更齐全,类型更完善。

  • 更规范的书写规则

    为了让代码逻辑更清晰,容易阅读,EWM加入了新的(很少的)配置字段和规则。例如,原生中methods字段下可以书写组件事件函数,内部方法 生命周期函数。EWM规则中,events字段书写组件事件函数,methods只书写内部方法,页面生命周期写在pageLifetimes字段下。这些规则是靠ts的类型来约束的。 如果您使用js开发,这些规则都是可选的。

  • 独立的子组件

    当组件中包含多个子组件时,把所有组件数据和方法都写在一起很不方便阅读和维护,小程序提供的Behavior存在字段重复和隐式依赖等问题,这都可以认为是js的原罪。EWM 提供的子组件构建函数(CreateSubComponent) 配合 TS 类型系统解决以上问题,让复杂组件更易写易维护。

  • 强大的类型系统

    EWM 拥有强大的类型推导系统,智能的字段提示、重复字段检测、类型检查,让错误被发现于书写代码时。

  • 支持任何第三方组件

    当你引入第三方UI库组件(无组件类型)时,您只要为引入的组件书写一个组件类型(IComponentDoc),即可引入到EWM体系中。EWM提供了内置的泛型CreateDoc,协助您书写第三方组件类型。

  • 完美兼容

    EWM 提供的API和类型系统基于原生,所以不存在兼容性,想用即用。

  • 对js友好 虽然TS开发下更能发挥EWM的特性,但只要您熟悉了EWM规则,使用js开发也是不错的选择,EWM中很多运行时检测是专为js而写的。

安装

  • 依赖安装(ts开发下)

    1. typescript npm i --save-dev typescript@^4.6.0 配置tsconfig.json
    {
      "compilerOptions": {
        // "lib": ["esnext"],最低es2015
        "module": "ES6",
        "strict": true,
        "moduleResolution": "node",
        "exactOptionalPropertyTypes": true
        // ...
      }
    }
    1. 官方ts类型

      npm i --save-dev @types/wechat-miniprogram

  • 安装 ewm

    1. npm安装: npm i ewm

    2. 配置文件: ewm.config.js(书写在node_modules同级目录下,配置规则)

    // 内部默认配置
    module.exports = {
      env: "development",
      language: "ts",
    };

    ⚠️ 不书写为内部默认配置,更改配置后,需要重新npm构建并清除缓存后生效。

  • mobx(可选)

    如果您不使用状态管理,可忽略安装

    安装 mobx npm i --save mobx

    当前mobx最新版本(当前为mobx@6),若要兼容旧系统(不支持proxy 比如ios9),请安装mobx@4 npm i -save mobx@4 注意: 因为小程序坏境无 process变量 在安装mobx@6 时 会报错process is not defined 需要在npm构建前更改 node_modules\mobx\dist\index.js如下

    原文件

    // node_modules\mobx\dist\index.js
    "use strict";
    
    if (process.env.NODE_ENV === "production") {
      module.exports = require("./mobx.cjs.production.min.js");
    } else {
      module.exports = require("./mobx.cjs.development.js");
    }

    开发环境可更改为

    // node_modules\mobx\dist\index.js
    module.exports = require("./mobx.cjs.development.js");

    生产环境可更改为

    // node_modules\mobx\dist\index.js
    module.exports = require("./mobx.cjs.development.js");

    与EWM配置文件关联写法如下

    let IsDevelopment = true;
    try {
      IsDevelopment = require("../../../../ewm.config").env === "development";
    } catch (error) {
    }
    if (IsDevelopment) {
      module.exports = require("./mobx.cjs.development.js");
    } else {
      module.exports = require("./mobx.cjs.production.min.js");
    }
  • 构建npm 开发者工具菜单——工具——构建npm

    详情见官方 npm 介绍

    tips:更改配置文件后,需要重新npm构建并清除缓存后生效

思想

  • 类型为先

    EWM 在设计各个配置字段或 API 时优先考虑的是能否契合TS的类型系统,这可能导致个别字段对于运行时来说是无意义的(生产环境会去掉)。因此相比js,使用ts开发更能发挥EWM的能力。比如 DefineComponent的path 字段,在js开发中可以忽略,但在ts开发下,此字段具有重要意义。

  • 类型即文档

    EWM中,实例构建函数(DefineComponent)返回的类型好比传统意义的组件文档,为引用组件时提供类型支持。EWM内置了原生(Wm)组件类型(暂不完善),对于第三方ui库组件,EWM会逐步拓展,为其提供类型支持(欢迎您的PR)。组件类型书写简单,您完全可以为自己的项目书写组件类型

    示例1

    示例中用到的类型可前往重要类型查看

    // 自定义组件Demo
    import { PropType, DefineComponent } from "ewm";
    export interface User {
      name: string;
      age?: number;
    }
    const demoDoc = DefineComponent({
      properties: {
        /**
         * @description num描述
         */
        num: Number,
        /**
         * @description str描述。
         */
        str: {
          type: String as PropType<"male" | "female">,
          value: "male",
        },
        /**
         * @description union描述
         */
        union: {
          type: Array as PropType<User[]>,
          value: { name: "zhao", age: 20 },
          optionalTypes: [Object as PropType<User>],
        },
      },
      customEvents: { // 字段书写规则请看 API——DefineComponent——customEvent。
        /**
         * @description 自定义事件customeEventA描述
         */
        customeEventA: String as PropType<"male" | "female">, // detailType为string类型 => 'male' | 'female'
        /**
         * @description 自定义事件customeEventB描述
         */
        customeEventB: [String, Number], // detailType为联合类型 => string | number
        /**
         * @description 自定义事件customeEventC描述
         */
        customeEventC: {
          detailType: Object as PropType<User>, // detailType为对象类型=> User
          options: { bubbles: true }, // 同原生写法
        },
        /**
         * @description 自定义事件customeEventD描述
         */
        customeEventD: {
          detailType: Array as unknown as PropType<[string, number]>, // detailType为元组类型 => [string,number]
          options: { bubbles: true, composed: true }, // 同原生写法
        },
        // ...
      },
      // ...
    });

    export type Demo = typeof demoDoc; // 导出组件类型

    // Demo 等效于 // type Demo = { // properties: { // num: number; // str?: { // type: "male" | "female"; // default: "male"; // }; // union?: { // type: User | User[]; // default: { // name: "zhao"; // age: 20; // }; // }; // }; // events: { // customeEventA: 'male' | 'female'; // customeEventB: string | number; // customeEventC: { // detailType:{name: string; age?: number }, // options:{ bubbles: true } // }; // customeEventD: { // detailType:string, number, // options:{ bubbles: true; composed: true } // }; // }; // };

    示例1中导出的类型 Demo 好比如下书写的组件描述文档
    
    | properties 属性 |   描述    |           默认值            |        类型         | 是否必传 |
    | :-----------: | :-----: | :----------------------: | :---------------: | :--: |
    |      num      |  num描述  |                          |      number       |  是   |
    |      str      |  str描述  |          "male"          | "male" \|"female" |  非   |
    |     union     | union描述 | { name: "zhao",age: 20 } |  User \| User[]   |  非   |
    
    |     自定义事件     |          描述          |            传递数据类型             |            options 配置            |
    | :-----------: | :------------------: | :---------------------------: | :------------------------------: |
    | customeEventA | 自定义事件customeEventA描述 |      'male' \| 'female'       |                                  |
    | customeEventB | 自定义事件customeEventB描述 |       string \| number        |                                  |
    | customeEventC | 自定义事件customeEventC描述 | {name: string, age?: number } |         { bubbles: true }         |
    | customeEventD | 自定义事件customeEventD描述 |       [string, number]        | { bubbles: true, composed: true } |
  • 关键数据和方法必须预声明

    原生开发时,子组件给父组件传值经常使用实例方法triggerEvent,这种写法不经意间把自定义事件名和配置隐藏在一些方法逻辑当中。不便重复调用,不易阅读,也无法导出类型。DefineComponent构建组件时中增加了customEvents字段用来书写自定义事件配置,方便代码阅读和类型导出。有些其他字段也基于此思想。例如DefineComponent构建页面时的publishEvents字段。

  • 严格的数据管控

    js开发或原生TS类型中,this.setData方法可以书写任何字段配置(或许data中原本没有声明的键名),不利于阅读,也不符合严格的单向数据流控制(组件应只能控制自身data字段),为避免造成数据混乱,EWM重写了setData的类型定义,要求输入配置时只能书写实例配置中data字段下(且非响应式字段)已定义的字段(除非使用as any 忽略TS类型检查),这也符合上面谈到思想————关键数据必须预声明。

    示例2

    import { PropType, DefineComponent } from "ewm";
    export interface User {
      name: string;
      age?: number;
    }
    DefineComponent({
      properties: {
        str: String,
        user: Object as PropType<User>,
      },
      data: {
        num: 100,
      },
      computed: {
        name(data) {
          return data.user.name;
        },
      },
      events: {
        onTap(e) {
          const str = this.data.str;
          const num = this.data.num;
          const user = this.data.user;
          this.setData({
            num: 200, // ok
            str: "string", // error  properteis属于父组件控制数据
            name: "zhang", // error 计算属性随内部依赖改变,不应在此修改。
          });
          // 不推荐做法
          this.setData({
            xxx: "anyType",
          } as any); // 跳过类型约束 不推荐
        },
      },
    });

特色预览

API

MainData

js开发可以忽略

书写复杂组件时,为了给单独书写的子组件模块提供主数据类型,需要将主数据抽离书写。 MainData函数只接受三个配置字段(properteis,data,computed)。

返回类型为IMainData:

interface IMainData {
  properties?: Record<string, any>; // 实际类型较复杂,这里简写了
  data?: Record<string, any>; // 实际类型较复杂,这里简写了
  computed?: Record<string, any>; // 实际类型较复杂,这里简写了
  allMainData?: Record<string, any>; // 实际类型较复杂,这里简写了
}

示例 3

import { PropType, DefineComponent } from "ewm";
interface User {
  name: string;
  age?: number;
}

const demoA = DefineComponent({
  properties: {
    a: String,
    user: Object as PropType<User>,
  },
  data: {
    b: 123,
  },
  computed: {
    name(data) {
      return data.user.name;
    },
  },
});

export type DemoA = typeof demoA;

示例 4

import { PropType, DefineComponent, MainData } from "ewm";

const mainData = MainData({
  properties: {
    a: String,
    user: Object as PropType<{ name: string; age?: number }>,
  },
  data: {
    b: 123,
  },
  computed: {
    name(data) {
      return data.user.name;
    },
  },
});

const demoB = DefineComponent({
  mainData,
  // ...
});

export type DemoB = typeof demoB;

DemoA和DemoB的类型完全一致,但在示例4中 主数据类型(typeof mainData)被单独提了出来,方便传递。

这是EWM中最遗憾的地方,暂时还没有更佳的实现方案,期待您给与指点。

DefineComponent

在EWM中实例(页面或组件)都是由DefineComponent函数构建的。 以下是对各个配置字段与原生规则不同之处的说明。在阅读说明前您可能需要了解官方 [Component 文档](https://developers.weixin.qq.com/miniprogram/dev/reference/api/Component.html)。

  • path(新增)

    js开发可忽略此字段。

    构建页面实例时(TS)此字段为返回组件类型一部分,类型为/${string} 例如: path:"/pages/index/index"

    运行时检测的报错信息:

    1. 当构建组件时,书写了path字段: [ ${组件路径} ] DefineComponent构建组件时,不应该书写path字段
    2. 当构建页面时 没有书写path字段或书写错误: [ ${页面路径} ] DefineComponent构建页面时,应书写path字段,值为 /${页面路径}
  • mainData(新增)

    js开发可忽略此字段。

    字段类型为IMainData,即[MainData](#mainData)函数返回值,书写此字段后,不可再书写properties、data、computed字段(类型变为never)。

  • properties

    DefineComponent会根据此字段配置推导出具体类型,做为组件类型的一部分。

    ⚠️组件类型严格区分必传和选传,辅助泛型[PropType](#PropType)

    1. 必传字段

      使用简写规则或不带 value 字段的全写规则(对象描述)。

      示例 5 简写必传字段

      import { PropType, DefineComponent } from "ewm";
      export interface User {
        name: string;
        age?: number;
      }
      export interface Cart {
        goodsName: string[];
        count: number;
      }
      const demoDoc = DefineComponent({
        properties: {
          str: String, // => string 简写
          strUnion: String as PropType<"red" | "black" | "white">, // =>  'red'|'black'|'white'
          num: Number, // => number
          numUnion: Number as PropType<100 | 200 | 300>, // =>  100 | 200 | 300
          bool: Boolean, // => boolean
          arr: Array, // => unknown[] 不推荐写法,描述过于宽泛
          arrUnion: Array as PropType<(string | number)[]>, // =>  (string|number)[]
          obj: Object, // => Record<string,any> 不推荐写法,描述过于宽泛
          objUnion: Object as PropType<User | Cart>, // => User | Cart
          tuple: Array as unknown as PropType<[Cart, User]>, // =>  [User,Cart] 唯一需要使用as unknown 的地方,
        },
      });
      export type DemoDoc = typeof demoDoc;
      // Demo1Doc的类型相当于
      // type DemoDoc = {
      // 	properties: {
      // 		str: string;
      // 		num: number;
      // 		bool: boolean;
      // 		strUnion: "red" | "black" | "white";
      // 		numUnion: 100 | 200 | 300;
      // 		arr: unknown[];
      // 		obj: {[x: string]: any};
      // 		arrUnion: (string | number)[];
      // 		objUnion: {
      // 			name: string;
      // 			age?: number;
      // 		} | {
      // 			goodsName: string[];
      // 			count: number;
      // 		};
      // 		tuple: [{
      // 			goodsName: string[];
      // 			count: number;
      // 		}, {
      // 			name: string;
      // 			age?: number;
      // 		}];
      // 	};
      // }

      ⚠️ 简写字段中的联合类型描述只限于同类型的联合, 比如 "red" | "black"100 | 200string[] | number[]User | Cart 都是同一原始类型的联合类型, 不同原始类型的联合(string|number)见示例 6。元组类型是唯一需要使用 as unknown 转译的

      示例 6 全写必传属性 当字段类型为不同原始类型的联合类型时,使用全写规则 全写规则下如果只写 type 字段(无 value 和 optionalTypes)效果和简写完全相同

      import { DefineComponent, PropType } from "ewm";
      export interface User {
          name: string;
          age?: number;
      }
      export interface Cart {
          goodsName:string[]
          count:number
      }
      const demoDoc = DefineComponent({
              str: { type: String },
              strUnion: { type: String as PropType<'red' | 'black' | 'white'> },
              num: { type: Number },
              numUnion: { type: Number as PropType<100 | 200 | 300> },
              bool: { type: Boolean },
              arr: { type: Array },
              arrUnion: { type: Array as PropType<(string | number)[]> },
              obj: { type: Object },
              objUnion: { type: Object as PropType<User | Cart> },
              tuple: { type: Array as unknown as PropType<[Cart, User]> },
              //以上就是示例5中必传字段的全写描述,效果同示例5的简写完全相同
       	   //以下是不同原始类型的联合写法
       	   str_number:{ type:String,optionalTypes:[Number] } //  => string | number 
       	   arr_obj: { type:Array as PropType<User[]>,optionalTypes:[Object as PropType<Cart>]} // => User[] | Cart
          }
      });
      export type DemoDoc = typeof demoDoc;
    2. 选传属性和默认值

      当书写全写规则时, 如果书写 value 字段, 表示属性为选传(生成的字段类型索引中会有?), value字段类型为返回类型中的default类型。当有写optionalTypes 字段, 返回类型为 type 类型和 optionalTypes 数组中各个类型的联合类型。value字段类型应为 type和optionalTypes的联合子类型 书写错误会报 Type 'xxxx' is not assignable to type 'never'.

      示例 7

      import { PropType, DefineComponent } from "ewm";
      
      export interface User {
        name: string;
        age?: number;
      }
      export interface Cart {
        goodsName: string[];
        count: number;
      }
      const demoDoc = DefineComponent({
        properties: {
          num: { type: Number, value: 123 }, // => { num?:{ type:number, default:123} }
          errorNum: { type: Number, value: "123" }, // => error `Type 'string' is not assignable to type 'never'.`
          str: { type: String, value: "123" }, // =>   { str?: { type:string, default:'123'} }
          bool: { type: Boolean, value: false }, // => { bool?: { type:boolean, default:false} }
          arr: {
            type: Array as PropType<number[]>,
            value: [1, 2, 3],
          }, // =>{ arr?:{type:number[],default:[1,2,3] } }
          obj: {
            type: Object as PropType<User>,
            value: { name: "zhao" },
          }, // => { obj?: {type:User,default:{ name: "zhao" }} }
          union: {
            type: Number,
            value: "string", // ok
            optionalTypes: [String, Object],
          }, // => { union?: { type: string | number | object; default: "string" } }
          union1: {
            type: Boolean,
            value: { name: "zhao" }, // ok
            optionalTypes: [
              Array as PropType<Cart[]>,
              Object as PropType<User>,
            ],
          }, //  { union1?: { type: boolean | Cart[] | User, default: {name:'zhao'}} }
          union2: {
            type: String as PropType<"a" | "b" | "c">,
            value: 123,
            optionalTypes: [
              Number as PropType<123 | 456>,
              Array as PropType<string[] | number[]>,
              Boolean,
              Object as PropType<User | Cart>,
            ],
          }, // {union2?: { type: 'a'|'b'|'c'| 123 | 456 | string[] | number[] | boolean |  Cart | User;  default: 123 }}
        },
      });
      export type DemoDoc = typeof demoDoc;
  • data

    新增 响应式数据字段(基于mobx)。 格式: "()=> observableObject.filed"

    示例 8

    import { DefineComponent } from "ewm";
    import { observable, runInAction } from "mobx";
    const user = observable({
      name: "zhao",
      age: 20,
    });
    setInterval(() => {
      runInAction(() => {
        user.name = "liu";
        user.age++;
      });
    }, 1000);
    DefineComponent({
      data: {
        name: user.name, // name字段非响应式写法,不具备响应式
        age: () => user.age, // age字段具有响应式 即当外部使user.age改变时,实例自动更新内部age为最新的user.age
      },
      lifetimes: {
        attached() {
          console.log(this.data.name, this.data.age); // "zhao",20
          setTimeout(() => {
            console.log(this.data.name, this.data.age); // "zhao" ,21
          }, 1000);
        },
      },
    });

    ⚠️ 当实例配置中(包含注入配置)存在响应式数据时,实例this下会生成disposer字段,类型为:{anyFields:stopUpdateFunc}。用以取消响应式数据同步更新,如 this._disposer.xxx() 则表示外部对xxx数据更改时,实例的xxx数据不再同步更新。如果实例没有响应式数据,则this._disposer为undefined。⚠️EWM在实例下加入的方法全部以下划线(``)开头。

    示例 8-1

    ⚠️一般情况下响应式数据的更新是在下一事件片段(wx.nextTick),即同一事件片段中的响应式数据会在下一次一起更新(一起setData)。

    import { DefineComponent } from "ewm";
    import { observable, runInAction } from "mobx";
    const times = observable({
      count1: 0,
      count2: 0,
      increaseCount1() {
        this.count1++;
      },
      increaseCount2() {
        this.count2++;
      },
    });
    DefineComponent({
      data: {
        count1: () => times.count1,
        count2: () => times.count2,
      },
      lifetimes: {
        attached() {
          times.increaseCount1();
          console.log(this.data.count1, this.data.count2); // 0 , 0
          times.increaseCount2();
          console.log(this.data.count1, this.data.count2); // 0 , 0
          setTimeout(() => {
            console.log(this.data.count1, this.data.count2); // 1 , 1
          }, 0);
        },
      },
    });

    如果您想立刻更新某一响应式数据(不等其他响应式数据一起更新),则可以执行实例下的_applySetData函数。

    示例 8-2

    import { DefineComponent } from "ewm";
    import { observable, runInAction } from "mobx";
    const times = observable({
      count1: 0,
      count2: 0,
      increaseCount1() {
        this.count1++;
      },
      increaseCount2() {
        this.count2++;
      },
    });
    DefineComponent({
      data: {
        count1: () => times.count1,
        count2: () => times.count2,
      },
      lifetimes: {
        attached() {
          times.increaseCount1();
          this._applySetData(); // 立即setData
          console.log(this.data.count1, this.data.count2); // 1 , 0
          times.increaseCount2();
          console.log(this.data.count1, this.data.count2); // 1 , 0
          setTimeout(() => {
            console.log(this.data.count1, this.data.count2); // 1 , 1
          }, 0);
        },
      },
    });
  • computed 与 watch

    同官方[miniprogram-computed](https://github.com/wechat-miniprogram/computed)

    示例 9

    import { PropType, DefineComponent } from "ewm";
    import { observable, runInAction } from "mobx";
    interface User {
      name: string;
      age: number;
    }
    interface Cart {
      count: number;
      averagePrice: number;
    }
    const store = observable({
      cart: <Cart> { count: 0, averagePrice: 10 },
    });
    DefineComponent({
      properties: {
        str: {
          type: String as PropType<"male" | "female">,
        },
        user: {
          type: Object as PropType<User>,
          value: { name: "zhao", age: 30 },
        },
      },
      data: {
        num: <123 | 456> 123,
        arr: [1, 2, 3],
        cart: () => store.cart,
      },
      computed: {
        name(data) {
          return data.user.name;
        },
        count(data) {
          return data.cart.count;
        },
      },
      watch: {
        // 监听 properteis数据
        str(newValue) {}, // newValue type => "male" | "female"
        // 监听 data
        num(newNum) {}, // newNum type =>  123 | 456
        arr(newArr) {}, // newArr type =>  number[]
        // 监听对象 默认`===`对比
        user(newUser) {}, // newUser type =>  User
        // 监听对象 深对比
        "user.**"(newUser) {}, // newUser type => User
        // 监听对象单字段
        "user.name"(newName) {}, // newName type =>  string
        "user.age"(newAge) {}, // newAge type =>  string
        "cart.count"(newCount) {}, // newCount => number
        // 监听双字段
        "num,arr"(cur_Num, cur_Arr) {}, // cur_Num => 123 | 456 ,cur_Arr => number[]
        // 监听注入响应字段
        injectTheme(newValue) {}, // newValue => "dark" | "light"
        // 监听data中响应字段 默认`===`对比
        cart(newValue) {}, // newValue => Cart
        // 监听data中响应字段 深对比
        "cart.**"(newValue) {}, // newValue => Cart
        // 监听计算属性字段 需要手写类型注解(鼠标放在字段(name)上-->看到参数类型-->手写类型)
        name(newName: string) {}, // newName => string
      },
    });

    ⚠️由于ts某些原因,watch字段下监听计算属性字段时,需要手写参数类型。参数类型可以通过把鼠标放在字段名上获取如上面中的watch下的name字段)。

  • subComponent 导入由CreateSubComponent建立的子模块,类型为:ISubComponent[]。 原生开发时,子组件给父组件传值通常使用实例上的 triggerEvent 方法.如下 示例 10

    // sonComp.ts
    import { DefineComponent } from "ewm";
    DefineComponent({
      methods: {
        onTap() {
          // ...
          this.triggerEvent("customEventA", "hello world", {
            bubbles: true,
            composed: true,
            capturePhase: true,
          });
        },
      },
    });
    <!-- parentComp.wxml  -->
    <sonComp bind:customEventA = "customEventA" />
    // parentComp.ts
    import { DefineComponent } from "ewm";
    DefineComponent({
      methods: {
        customEventA(e: WechatMiniprogram.CustomEvent) {
          console.log(e.detail); //  'hello world'
        },
      },
    });

    EWM写法

    示例 11

    // Components/subComp/subComp.ts
    import { DefineComponent } from "ewm";
    
    const subDoc = DefineComponent({
      properties: {
        // ...
      },
      customEvents: { // 定义自定义事件
        customEventA: String,
        customEventB: {
          detailType: Array as PropType<string[]>,
          options: { bubbles: true },
        },
        customEventC: {
          detailType: [Array as PropType<string[]>, String], // 多类型联合写在数组中
          options: { bubbles: true, composed: true },
          // ...
        },
      },
      methods: {
        ontap() {
          // 直接触发,参数类型为customEvents中定义的类型,配置自动加入。
          this.customEventA("hello world"); // ok  等同于 this.triggerEvent('customEventA','hello world')
          this.customEventA(123); // error    类型“number”的参数不能赋给类型“string”的参数
          this.customEventB(["1", "2", "3"]); // ok 等同于 this.triggerEvent('customEventA','hello world',options:{ bubbles:true })
          this.customEventB([1, 2, 3]); // error 不能将类型“number”分配给类型“string”
          this.customEventC("string"); // ok 等同于 this.triggerEvent('customEventA','string',options:{ bubbles:true ,composed: true})
          this.customEventC(["a", "b", "c"]); // ok 等同于 this.triggerEvent('customEventA',['a','b','c'],options:{ bubbles:true ,composed: true})
          this.customEventC(true); // error  类型“boolean”的参数不能赋给类型“string | string[]”的参数
        },
      },
    });
    
    export type Sub = typeof subDoc;
    <!-- parentComp.wxml -->
    <view >
    	<sonComp bind:customEventA = "customEventA" bind:customEventB = "customEventB" />
    </view>

    示例 12

    // Components/Parent/Parent.ts
    import { Sub } from "Components/subComp/subComp";
    import { CreateSubComponent, DefineComponent } from "ewm";
    
    const subComp = CreateSubComponent<{}, Sub>()({
      // ...子组件数据和方法
    });
    
    const parentDoc = DefineComponent({
      subComponent: [subComp], // 通过subComponent字段引入子组件(类型)
      events: {
        customEventA(e) { //  e => WechatMiniprogram.CustomEvent
          console.log(e.detail); // => 'hello world'
        },
        customEventB(e) {
          console.log(e.detail); // => ['1','2','3']
        },
        customEventC(e) {
          console.log(e.detail); // =>  'string' , ['a','b','c']
        },
      },
    });
    
    export type Parent = typeof parentDoc;
    // Parent 等效于 { customEventC: { detailType:string | string[],options:{ bubbles: true, composed: true }} 因为Sub中定义的customEventC事件是冒泡并穿透的,Parent会继承类型。

    小结: 组件间传值时子组件应该把自定义事件配置定义在customEvents字段中。父组件会在events字段中得到子组件的自定义事件类型。

  • events

    组件事件函数字段(包含子组件自定义事件)。 类型: {[k :string]:(e:WechatMiniprogram.BaseEvent)=>void } ⚠️内部自动导入 subComponent字段中的子组件事件类型,方便获取代码提示。 events字段类型没有加入到this上,因为events是系统事件。

  • pageLifetimes

    原生中小程序使用Component构建组件时,pageLifetimes子字段为:show、hide、resize,EWM拓展为同页面生命周期一样字段 onHide、onShow、onResiz。 原生中小程序使用Component构建页面时,要求把页面生命周期写在methods下, EWM改为还写在pageLifetimes字段中。

    小结: EWM页面生命周期永远写在pageLifetimes下,组件实例中只提示3个字段(onHide、onShow、onResiz),页面实例提示全周期字段。js开发下此规则可选。 示例 13

    // components/test/test
    import { DefineComponent } from "ewm";
    // 构建组件
    const customComponent = DefineComponent({
      pageLifetimes: { // 组件下只开启3个字段
        onShow() {
          // ok
        },
        onHide() {
          // ok
        },
        onResize() {
          // ok
        },
        onLoad() {
          // 报错 不支持的字段
        },
        onReady() {
          // 报错 不支持的字段
        },
      },
    });

    示例 14

    // pages/index/index
    import { DefineComponent } from 'ewm';
    const indexPage = DefineComponent({
    	path:"/pages/index/index"
    	pageLifetimes: { //因为书写path字段表示构建的是页面实例,会开启全字段
    		onLoad() {
    			//ok 
    		},
    		onReady(){
    			// ok 
    		}
    		onShow() {
    			// ok
    		},
    		onHide() {
    			// ok
    		},
    		onResize() {
    			// ok
    		},
    		//...
    
    	},
    });
  • publishEvents和subscribeEvents

    原生开发中当前页通过[wx.navigateTo](https://developers.weixin.qq.com/miniprogram/dev/api/route/wx.navigateTo.html)等方法给下级页面传值,无法进行类型检测。为此EWM提供了实例方法navigateTo,除此之外EWM还提供了新的页面间通信方案。

    publishEvents: 页面发布事件定义字段,定义了path字段时开启。

    subscribeEvents: 页面响应其他页面发布事件的函数字段,定义了path字段时开启。

    [js示例18](#jspublish)

    示例 15

    // pages/index/index.ts
    import { DefineComponent } from "ewm";
    import { PageA } from "../PageA/PageA";
    import { PageB } from "../PageB/PageB";
    
    DefineComponent({
      path: "/pages/index/index",
      subscribeEvents(Aux) { // 订阅事件字段为函数字段,辅助函数Aux方便类型引入
        return Aux<[PageA, PageB]>({ // 订阅多个页面发布事件,写数组 IPageDoc[]
          "/pages/PageA/PageA": { // 订阅 PageA页面发布的事件 publishA
            publishA: (data) => {
              console.log(data);
              // 'first_publishA'   打印顺序 2
              // 'second_publishA'  打印顺序 3
            },
          },
          "/pages/PageB/PageB": { // 订阅 PageB页面发布的事件 publishB
            publishB: (data) => {
              console.log(data); // [" first_pbulishB"]  打印顺序 5
              return false; // 关闭订阅 即只接收一次发布事件(内部删除此函数)
            },
          },
        });
      },
      pageLifetimes: {
        onLoad() {
          this.navigateTo<PageA>({ // 跳转到页面PageA
            url: "/pages/PageA/PageA",
            data: { fromPageUrl: this.is }, // 支持传递特殊字符  ; / ? : @ & = + $ , #
          }).then((res) => {
            console.log(res.errMsg); //  "navigateTo:ok "   打印顺序 1
          });
        },
      },
    });

    示例 16

    // pages/PageA/PageA.ts
    import { PropType, DefineComponent } from "ewm";
    import { PageB } from "../PageB/PageB";
    
    const pageADoc = DefineComponent({
      path: "/pages/PageA/PageA",
      properties: { // 定义页面接收的数据类型,与组件不同之处在于非响应式,即页面只在onLoad时接收传值。
        fromPageUrl: String,
      },
      publishEvents: { // 定义一个发布事件,事件名 publishA 参数为string
        publishA: String,
      },
      subscribeEvents(h) { // 订阅事件字段
        return h<PageB>({
          "/pages/PageB/PageB": { // 订阅PageB页面发布的事件
            publishB: (data) => {
              console.log(data);
              // [first_pbulishB] 打印顺序 6
              // second_pbulishB 打印顺序 7
            },
          },
        });
      },
      pageLifetimes: {
        onLoad(data) { // data类型同Properties字段 =>  {  fromPageUrl: string; }
          const url = this.is; // '/pages/PageA/PageA'
          this.publishA("first_publishA"); // 第一次 发布 publishA 事件
          this.navigateTo<PageB>({ // 跳转到PageB页面
            url: "/pages/PageB/PageB",
            data: { fromPageUrl: url },
          }).then(() => {
            this.publishA("second_publishA"); // 第二次 发布 publishA 事件
          });
        },
      },
    });
    export type PageA = typeof pageADoc;

    示例 17

    // pages/PageB/PageB.ts
    import { PropType, DefineComponent } from "ewm";
    
    const pageBDoc = DefineComponent({
      path: "/pages/PageB/PageB",
      properties: {
        fromPageUrl: String,
      },
      publishEvents: { // 发布事件名 publishB,联合类型写成数组形式
        publishB: [String, Array as PropType<string[]>], // type => string | string[]
      },
      pageLifetimes: {
        onLoad(data) { // 类型同properties字段
          console.log(data.fromPageUrl); // "pages/PageA/PageA" 打印顺序 4
          this.publishB(["first_pbulishB"]); //  第一次发布
          this.publishB("second_pbulishB"); // 第二次发布
        },
      },
    });
    export type PageB = typeof pageBDoc;

    js开发时可以如下书写

    示例 18

    // pages/otherPage/otherPage.ts
    import { DefineComponent } from "ewm";
    
    DefineComponent({
      properties: {
        fromPageUrl: String,
      },
      publishEvents: {
        /**
         * 定义一个发布事件 名为publishA,传值类型为string
         */
        publishA: String,
        /**
         * 定义一个发布事件 名为publishA,传值类型为string | array
         */
        publishB: [String, Array],
      },
      pageLifetimes: {
        onLoad(data) { // 类型同properties字段
          console.log(data.fromPageUrl); // "pages/index/index" 打印顺序 2
          this.publishA("first"); // 第一次发布
          this.publishA("second"); // 第二次发布
          this.publishB("first"); //  第一次发布
          this.publishB(["second"]); // 第二次发布
        },
      },
    });

    示例 19

    // pages/index/index.ts 首页
    import { DefineComponent } from "ewm";
    
    DefineComponent({
      subscribeEvents() {
        return {
          "/pages/OtherPage/OtherPage": { // 订阅OtherPage页面发布的事件
            publishA: (data) => {
              console.log(data); // 'first' 打印顺序 3  'second' 打印顺序 4
            },
            publishB: (data) => {
              console.log(data); // 'first'  打印顺序 5
              return false; // 关闭订阅 即只接收一次发布事件(内部删除此函数)
            },
          },
          // ...
        };
      },
      pageLifetimes: {
        onLoad() {
          this.navigateTo({ // 跳转到页面OtherPage
            url: "/pages/OtherPage/OtherPage",
            data: { fromPageUrl: this.is }, // 支持传递特殊字符  ; / ? : @ & = + $ , #
          }).then((res) => {
            console.log(res.errMsg); //  "navigateTo:ok "   打印顺序 1
          });
        },
      },
    });

    ⚠️ 子事件函数应写成箭头函数。页面实例被摧毁时会自解除事件订阅。

  • DefineComponent的第二个参数

    书写DefineComponent配置时,建议传入第二个参数(类型为字符串),做为输出类型的前缀,导出的类型字段前将加入 ${string}_可有效避免与其他字段重复。

    示例 20

    // components/tabbar/tabbar.ts
    import { defineComonent } from "ewm";
    const tabbar = DefineComponent({
      properties: {
        str: String,
        num: Number,
      },
      customEvents: {
        eventA: Number,
      },
    }); // ⚠️无第二个参数
    export type Tabbar = typeof tabbar;
    // Tabbar 等效于
    // type Tabbar = {properties:{ str:string,num:number}; events:{ eventA: number}; }

    示例 21

    // components/button/button.ts
    import { defineComonent } from "ewm";
    const button = DefineComponent({
      properties: {
        str: String,
        num: Number,
      },
      customEvents: {
        eventA: Number,
        eventB: String,
      },
    }, "button"); // ⚠️推荐 以组件名为组件类型前缀
    export type Button = typeof button;
    // Button 等效于
    // type Button = {properties:{ button_str:string,num:number}; events:{ button_eventA: number; button_eventB: string}; }

createSubComponent

用于组件中包含多个子组件时,构建独立的子组件模块。

⚠️由于当前ts内部和外部泛型共用时有冲突,createSubComponent设计为高阶函数,需要两次调用,在第二次调用中书写配置,切记。

CreateSubComponent接受三个泛型(以下提到的泛型即这里的泛型),类型分别为 IMainData(MainData函数返回类型,可输入'{}'占位),IComponentDoc(DefineComponent返回类型(IComopnentDoc),可输入{}占位),Prefix(字符串类型,省缺为空串)。

当输入Prefix时(例如'aaa'),若第二个泛型为中无字段前缀,则要求配置内部字段前缀为'aaa',若第二个泛型有前缀字段(例如:'demoA'),则要求配置内部字段前缀为 'demoA_aaa'

CreateSubComponent 还可以用以制作相同逻辑代码的抽离(behaviors),此时第一个泛型与第二个泛型均为{},输入第三个泛型(逻辑名称)做前缀避免与其他behavior字段重复。

不用担心书写的复杂,因为EWM配置字段都有字段提示的,甚至在加了前缀的情况下比无前缀情况下,更便于书写。

示例22 前缀规则

 <!-- parentDemo.wxml -->
<view  >

	<button id='0' str="{{button_0_str}}" str="{{button_0_str}}"/>	
	<button id='1' str="{{button_1_str}}" str="{{button_1_num}}"/>	
	<tabbar str="{{tabbar_str}}" num="{{tabbar_num}}" />
<view />

示例 23

// components/demo/demo.ts
import { CreateSubComponent, DefineComonent, MainData } from "ewm";
import { Button } from "./components/button/button"; //  示例 21
import { Tabbar } from "./components/tabbar/tabbar"; //  示例 20
const tabbar = CreateSubComponent<typeof mainData, Tabbar, "tabbar">()({ // 第二泛型Tabbar无前缀,第三泛型为'tabbar',最终配置字段前缀为tabbar_
  data: {
    // str: 'string', //  error ⚠️此字段要求前缀为tabbar⚠️       前缀检测
    // tabbar_str: 123, // error 不能将"number"赋值给"string"     类型检测
    tabbar_str: "string", // ok
  },
  computed: {
    tabbar_num(data) { // data中包含自身数据、主数据和注入数据  ok
      return data.user.name;
    },
    // tabbar_xxx(data) { // error  xxx不属于子组件字段          超出字段检测
    // 	return data.user.name;
    // },
  },
});
const button0 = CreateSubComponent<typeof mainData, Button, "0">()({ // 第二泛型Button有前缀"button",第三泛型为'0'最终配置字段前缀为 button_0_
  data: {
    button_0_str: "string", // ok
  },
  computed: {
    // button_num(data) { // error ⚠️此字段要求前缀为button_0_⚠️
    // 	return data.user.age;
    // },
    button_0_num(data) { // ok
      return data.user.age;
    },
  },
});
const button1 = CreateSubComponent<typeof mainData, Button, "1">()({ // 第二泛型DemoB有前缀"button",第三泛型为'1'最终配置字段前缀为 button_1_
  data: {
    button_1_str: "string", // ok
  },
  computed: {
    button_1_num(data) { // ok
      return data.user.age;
    },
  },
});
const ViewA = CreateSubComponent<{}, {}, "viewIdA">()({ // 第二泛型无前缀, 第三泛型前缀为"viewIdA" 最终配置字段前缀为 viewIdA_
  data: {
    viewIdA_xxx: "string",
    viewIdA_yyy: 123,
  },
});
const mainData = MainData({
  properties: {
    user: Object as PorpType<{ name: string; age: number }>,
  },
  data: {
    age: 123,
  },
  computed: {
    name(data) {
      return data.user.name;
    },
  },
});
const demo = DefineComponent({
  mainData,
  subComopnent: [tabbar, button0, button1, ViewA],
  events: {
    tabbar_eventA(e) {
      console.log(e.detail); // number
    },
    button_0_eventA(e) {
      console.log(e.detail); // number
    },
    button_1_eventB(e) {
      console.log(e.detail); // string
    },
  },
  // ...
});
export type Demo = typeof demo;
  • properties

    当希望子组件类型的properties字段由当前组件调用者(爷爷级)提供数据时书写此字段。类型的索引为子组件类型索引字段,值类型可更改必选或可选,值类型为子组件类型的子集。字段会被主组件继承导出。

    若给子组件传值为wxml提供时(比如子组件数据由wxml上层组件循环产生子数据提供时) 值类型应写为wxml,此字段不会被主组件继承,运行时会忽略此字段。

    <!-- /components/home/home -->
    <view  >
          <tabbar str="{{tabbar_str}}" num="{{tabbar_num}}" />
    		<block wx:for="{{[1,2,3,4]}}}" wx:key="index">
      								<!-- num值并非.ts提供,而有wxml提供 -->
    		<button  str="{{button_str}}" num="{{item}}" />
    	</block>
    <view />
     // components/home/home 
    import { CreateSubComponent, DefineComonent, MainData } from 'ewm';
    import { Tabbar } from './components/tabbar/tabbar'; //  示例 20
    import { Button } from './components/button/button'; //  示例 21
    const tabbar = CreateSubComponent<typeof mainData, Tabbar,'tabbar'>()({
    properties: {
    	tabbar_str: { //给子组件传了一个string,并继续交由上级控制。必传变为了可选
    		type:String,
    		value:'string'
    	}
      tabbar_num: Number, //直接交由上级控制赋值。 还是必传字段
      // demoA_xxx:"anyType" // error 不属于子组件proerties范围内  超出字段检测     
      },
     });
    const button = CreateSubComponent<typeof mainData, Button>()({
      properties: {
      button_num: 'wxml', //表示 子组件的num字段由wxml提供。
      },
      data: {
      	//  button_num:123 // error 字段重复因为在properteis中已有了button_num字段  重复字段检测。
      	button_str: 'string', // ok
      }
    });
    const home = DefineComponent({
     subComponet:[tabbar,button]
    });
    export type Home = typeof home
  • data

    类型为 子组件字段排除properties中已写字段的其他字段。有重复字段检测和前缀检测。

  • computed

    类型为 子组件字段排除properties和data中已写字段的其他字段。有重复字段检测和前缀检测和超出字段检测。

  • externalMethods

    暴漏给主逻辑调用的接口,主逻辑控制子模块的通道。前缀检测,重复字段检测

    import { CreateSubComponent, DefineComonent, MainData } from "ewm";
    import { Tabbar } from "./components/tabbar/tabbar"; //  示例 20
    const tabbar = CreateSubComponent<typeof mainData, tabbar, "tabbar">()({
      properties: {
        tabbar_str: { // 给子组件tabbar_str传了一个默认值string,并继续交由上级控制。
          type: String,
          value: "string",
        },
      },
      data: {
        tabbar_num: 123, // 给子组件初始值为 123
      },
      externalMethods: {
        tabbar_changeNum(num: number) { // 由主模块调用的接口,添加在主模块this方法上
          this.setData({
            tabbar_num, // 456
          });
        },
      },
    });
    const demo = DefineComponent({
      subComponet: [tabbar],
      lifetimes: {
        attached() {
          this.tabbar_changeNum(456); // 通过子组件暴漏接口给子组件传递数据。
        },
      },
    });
    export type Demo = typeof demo;

InstanceInject

全实例注入类

  1. 书写注入文件

    // inject.ts
    import { observable, runInAction } from "mobx";
    import { InstanceInject } from "./src/core/instanceInject";
    
    // 注入全局数据
    const globalData = { user: { name: "zhao", age: 20 } };
    // 注入的响应式数据
    const themeStore = observable({ theme: wx.getSystemInfoSync().theme }); // 记得开启主题配置(app.json  "darkmode": true),不然值为undefined
    
    wx.onThemeChange((Res) => {
      runInAction(() => {
        themeStore.theme = Res.theme;
      });
    });
    
    // 注入的方法
    function injectMethod(data: string) {
      console.log(data);
    }
    // 书写注入配置
    InstanceInject.InjectOption = {
      data: {
        injectTheme: () => themeStore.theme,
        injectGlobalData: globalData,
      },
      options: {
        addGlobalClass: true,
        multipleSlots: true,
        pureDataPattern: /^_/,
      },
      methods: {
        injectMethod,
      },
    };
    // 声明注入类型 js开发可以忽略
    declare module "ewm" {
      interface InstanceInject {
        data: {
          injectTheme: () => NonNullable<typeof themeStore.theme>;
          injectGlobalData: typeof globalData;
        };
        methods: {
          injectMethod: typeof injectMethod;
        };
      }
    }
    1. 导入注入文件
    // app.ts
    import "./path/inject";
    App({});
    1. 使用注入数据
    //ComponentA.ts
    import {DefineComponent} from "ewm";
    DefineComponent({
    	methods:{
    		onTap(){
    			console.log(this.data.globalData); //{ user: { name: "zhao", age: 20 } }
    			console.log(this.data.theme); // "dark" | "light"  响应式数据
    			console.log(this.injectMethod) //(data:string)=>void
    		}
    	},
    	lifetimes: {
    		attached() {
    			console.log(this.data.globalData); //{ user: { name: "zhao", age: 20 } }
    			console.log(this.data.theme); // "dark" | "light"  响应式数据
    			console.log(this.injectMethod) //(data:string)=>void
    		}
    	};
    })

重要类型

PropType

常用于辅助书写properties字段和customEvent字段类型

```ts
declare type PropType<T = any> = {
	new (...arg: any[]): T;
} | {
	(): T;
};
```

IEwmConfig

EWM配置文件类型

export interface IEwmConfig {
  /**
   * @default 'development'
   * @description 生产环境会去掉运行时检测等功能。
   */
  env?: "development" | "production";
  /**
   * @default 'ts'
   * @description ts环境会关闭一些运行时检测。
   */
  language?: "ts" | "js";
}

CreateDoc

import { CreateDoc } from "ewm";

type Color = `rgba(${number}, ${number}, ${number}, ${number})` | `#${number}`;
type ChangeEventDetail = {
  current: number;
  currentItemId: string;
  source: "touch" | "" | "autoplay";
};
type AnimationfinishEventDetail = ChangeEventDetail;
export type Swiper = CreateDoc<{
  properties: {
    /**
     * 是否显示面板指示点
     */
    indicator_dots?: {
      type: boolean;
      default: false;
    };
    /**
     * 指示点颜色
     */
    indicatorColor?: {
      type: Color;
      default: "rgba(0, 0, 0, .3)";
    };
    /**
     * 当前选中的指示点颜色
     */
    indicatorActiveColor?: {
      type: Color;
      default: "#000000";
    };
    /**
     * 是否自动切换
     */
    autoplay?: {
      type: boolean;
      default: false;
    };
    /**
     * 当前所在滑块的 index
     */
    current?: {
      type: number;
      default: 0;
    };
    /**
     * 自动切换时间间隔
     */
    interval?: {
      type: number;
      default: 5000;
    };
    /**
     * 滑动动画时长
     */
    duration?: {
      type: number;
      default: 500;
    };
    /**
     * 是否采用衔接滑动
     */
    circular?: {
      type: boolean;
      default: false;
    }; /**
     * 滑动方向是否为纵向
     */
    vertical?: {
      type: boolean;
      default: false;
    };
    /**
     * 前边距,可用于露出前一项的一小部分,接受 px 和 rpx 值
     */
    previousMargin?: {
      type: string;
      default: "0px";
    };
    /**
     * 后边距,可用于露出后一项的一小部分,接受 px 和 rpx 值
     */
    nextMargin?: {
      type: string;
      default: "0px";
    };
    /**
     * 当 swiper-item 的个数大于等于 2,关闭 circular 并且开启 previous-margin 或 next-margin 的时候,可以指定这个边距是否应用到第一个、最后一个元素
     */
    snapToEdge?: {
      type: boolean;
      default: false;
    };
    /**
     * 同时显示的滑块数量
     */
    displayMultipleItems?: {
      type: number;
      default: 1;
    };
    /**
     * 指定 swiper 切换缓动动画类型
     */
    easingFunction?: {
      type:
        | "default"
        | "linear"
        | "easeInCubic"
        | "easeOutCubic"
        | "easeInOutCubic";
      default: "default";
    };
  };
  events: {
    /**
     * current 改变时会触发 change 事件,event.detail = {current, source}
     */
    change: ChangeEventDetail;
    /**
     * swiper-item 的位置发生改变时会触发 transition 事件,event.detail = {dx: dx, dy: dy}
     */
    transition: { dx: number; dy: number };
    /**
     * animationfinish 动画结束时会触发 animationfinish 事件,event.detail change字段
     */
    animationfinish: AnimationfinishEventDetail;
  };
}, "swiper">;

提示: 强烈推荐使用组件名做为第二个泛型参数('swiper'),返回的子字段键类型会加入前缀("swiper_")

鸣谢

[TSRPC](https://github.com/k8w/tsrpc) 作者@k8w

@geminl @scriptpower @yinzhuoei的无私帮助

0.11.26

3 years ago

0.3.2

4 years ago

0.3.1

4 years ago

0.3.0

4 years ago

0.1.20

4 years ago

0.1.19

4 years ago

0.1.18

4 years ago

0.1.17

4 years ago

0.1.16

4 years ago

0.1.15

4 years ago

0.1.14

4 years ago

0.1.13

4 years ago

0.1.12

4 years ago

0.1.11

4 years ago

0.1.10

4 years ago

0.1.9

4 years ago

0.1.8

4 years ago

0.1.7

4 years ago

0.1.6

4 years ago

0.1.5

4 years ago

0.1.4

4 years ago

0.1.3

4 years ago

0.1.2

4 years ago

0.1.1

4 years ago

0.1.0

4 years ago

0.0.7

4 years ago

0.0.6

4 years ago

0.0.5

4 years ago

0.0.4

4 years ago

0.0.3

4 years ago

0.0.2

4 years ago