ewm
TypeScript icon, indicating that this package has built-in type declarations

0.8.9-dev.20220411 • Public • Published

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 { AuxType, DefineComponent } from 'ewm';
    export interface User {
    	name: string;
    	age?: number;
    }
    const demoDoc = DefineComponent({
    	properties: {
    		/**
    		 * @description num描述
    		 */
    		num: Number,
    		/**
    		 * @description str描述。
    		 */
    		str: {
    			type: String as AuxType<'male' | 'female'>,
    			value: 'male',
    		},
    		/**
    		 * @description union描述
    		 */
    		union: {
    			type: Array as AuxType<User[]>,
    			value: { name: 'zhao', age: 20 },
    			optionalTypes: [Object as AuxType<User>],
    		},
    	},
    	customEvents: { //字段书写规则请看 API——DefineComponent——customEvent。
    		/**
    		 * @description 自定义事件customeEventA描述
    		 */
    		customeEventA: String as AuxType<'male' | 'female'>, // detailType为string类型 => 'male' | 'female'
    		/**
    		 * @description 自定义事件customeEventB描述
    		 */
    		customeEventB: [String, Number], // detailType为联合类型 => string | number
    		/**
    		 * @description 自定义事件customeEventC描述
    		 */
    		customeEventC: {
    			detailType: Object as AuxType<User>, // detailType为对象类型=> User
    			options: { bubbles: true }, //同原生写法
    		},
    		/**
    		 * @description 自定义事件customeEventD描述
    		 */
    		customeEventD: {
    			detailType: Array as unknown as AuxType<[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 } { bubble: true }
    customeEventD 自定义事件customeEventD描述 [string, number] { bubble: true, composed: true }
  • 关键数据和方法必须预声明

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

  • 严格的数据管控

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

    示例2

    import { AuxType, DefineComponent } from 'ewm';
    export interface User {
    	name: string;
    	age?: number;
    }
    DefineComponent({
    	properties: {
    		str: String,
    		user: Object as AuxType<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 { AuxType, DefineComponent } from 'ewm';
interface User {
	name: string;
	age?: number;
}

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

export type DemoA = typeof demoA;

示例 4

import { AuxType, DefineComponent, MainData } from 'ewm';

const mainData = MainData({
	properties: {
		a: String,
		user: Object as AuxType<{ 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 文档

  • path(新增)

    js开发可忽略此字段。

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

    运行时检测的报错信息:

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

    js开发可忽略此字段。

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

  • properties

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

    ⚠️组件类型严格区分必传和选传,辅助泛型AuxType

    1. 必传字段

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

      示例 5 简写必传字段

      import { AuxType, 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 AuxType<'red' | 'black' | 'white'>, // =>  'red'|'black'|'white'
      		num: Number, // => number
      		numUnion: Number as AuxType<100 | 200 | 300>, // =>  100 | 200 | 300
      		bool: Boolean, // => boolean
      		arr: Array, // => unknown[] 不推荐写法,描述过于宽泛
      		arrUnion: Array as AuxType<(string | number)[]>, // =>  (string|number)[]
      		obj: Object, // => Record<string,any> 不推荐写法,描述过于宽泛
      		objUnion: Object as AuxType<User | Cart>, // => User | Cart
      		tuple: Array as unknown as AuxType<[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, AuxType } 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 AuxType<'red' | 'black' | 'white'> },
              num: { type: Number },
              numUnion: { type: Number as AuxType<100 | 200 | 300> },
              bool: { type: Boolean },
              arr: { type: Array },
              arrUnion: { type: Array as AuxType<(string | number)[]> },
              obj: { type: Object },
              objUnion: { type: Object as AuxType<User | Cart> },
              tuple: { type: Array as unknown as AuxType<[Cart, User]> },
              //以上就是示例5中必传字段的全写描述,效果同示例5的简写完全相同
       	   //以下是不同原始类型的联合写法
       	   str_number:{ type:String,optionalTypes:[Number] } //  => string | number 
       	   arr_obj: { type:Array as AuxType<User[]>,optionalTypes:[Object as AuxType<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 { AuxType, 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 AuxType<number[]>,
      			value: [1, 2, 3],
      		}, // =>{ arr?:{type:number[],default:[1,2,3] } }
      		obj: {
      			type: Object as AuxType<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 AuxType<Cart[]>,
      				Object as AuxType<User>,
      			],
      		}, //  { union1?: { type: boolean | Cart[] | User, default: {name:'zhao'}} }
      		union2: {
      			type: String as AuxType<'a' | 'b' | 'c'>,
      			value: 123,
      			optionalTypes: [
      				Number as AuxType<123 | 456>,
      				Array as AuxType<string[] | number[]>,
      				Boolean,
      				Object as AuxType<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

    示例 9

    import { AuxType, 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 AuxType<'male' | 'female'>,
    		},
    		user: {
    			type: Object as AuxType<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 AuxType<string[]>, options: { bubbles: true } },
    		customEventC: {
    			detailType: [Array as AuxType<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 { CreateSubComponent, DefineComponent } from 'ewm';
    import { Sub } from 'Components/subComp/subComp';
    
    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等方法给下级页面传值,无法进行类型检测。为此EWM提供了实例方法navigateTo,除此之外EWM还提供了新的页面间通信方案。

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

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

    js示例18

    示例 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 { AuxType, 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 { AuxType, DefineComponent } from 'ewm';
    
    const pageBDoc = DefineComponent({
    	path: '/pages/PageB/PageB',
    	properties: {
    		fromPageUrl: String,
    	},
    	publishEvents: { //发布事件名 publishB,联合类型写成数组形式
    		publishB: [String, Array as AuxType<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 { Tabbar } from './components/tabbar/tabbar'; //  示例 20
import { Button } from './components/button/button'; //  示例 21
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
    		}
    	};
    })

重要类型

AuxType

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

```ts
declare type AuxType<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 作者@k8w

@geminl @scriptpower @yinzhuoei的无私帮助

赞助

微信 支付宝

ewm探讨群

群名片 若失效可在官方论坛私信 Zhao ZW

Readme

Keywords

Package Sidebar

Install

npm i ewm

Weekly Downloads

8

Version

0.8.9-dev.20220411

License

MIT

Unpacked Size

250 kB

Total Files

27

Last publish

Collaborators

  • missannil