概述
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开发下)
-
typescript
npm i --save-dev typescript@^4.6.0
配置tsconfig.json
{ "compilerOptions": { //"lib": ["esnext"],最低es2015 "module": "ES6", "strict": true, "moduleResolution": "node", "exactOptionalPropertyTypes": true //... } }
-
官方ts类型
npm i --save-dev @types/wechat-miniprogram
-
typescript
-
安装 ewm
-
npm安装:
npm i ewm
-
配置文件: 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); // 跳过类型约束 不推荐 }, }, });
特色预览
-
新的实例间传值方式
-
中文错误提示
EWM在错误提示中加入了中文字段(在
⚠️
符号之间),方便快速找到错误原因。例如:⚠️与注入的data字段重复⚠️
⚠️ 有时TS会把错误标记到上级字段,实际为子字段报错! 解决报错应从上到下,由内而外,另外不符合EWM的tsconfig.json配置也可能导致类型错误。
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;
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"
运行时检测的报错信息:
- 当构建组件时,书写了path字段:
[ ${组件路径} ] DefineComponent构建组件时,不应该书写path字段
- 当构建页面时 没有书写path字段或书写错误:
[ ${页面路径} ] DefineComponent构建页面时,应书写path字段,值为 /${页面路径}
- 当构建组件时,书写了path字段:
-
mainData(新增)
js开发可忽略此字段。
字段类型为IMainData,即MainData函数返回值,书写此字段后,不可再书写properties、data、computed字段(类型变为never)。
-
DefineComponent会根据此字段配置推导出具体类型,做为组件类型的一部分。
⚠️ 组件类型严格区分必传和选传,辅助泛型AuxType-
必传字段
使用简写规则或不带 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 | 200
或string[] | 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;
-
选传属性和默认值
当书写全写规则时, 如果书写 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;
-
-
新增 响应式数据字段(基于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); }, }, });
-
示例 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 }, //... }, });
-
原生开发中当前页通过wx.navigateTo等方法给下级页面传值,无法进行类型检测。为此EWM提供了实例方法navigateTo,除此之外EWM还提供了新的页面间通信方案。
publishEvents: 页面发布事件定义字段,定义了path字段时开启。
subscribeEvents: 页面响应其他页面发布事件的函数字段,定义了path字段时开启。
示例 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;
示例 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
-
书写注入文件
// 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; }; } }
- 导入注入文件
// app.ts import './path/inject'; App({});
- 使用注入数据
//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