ewm
Version:
小程序原生插件
1,616 lines (1,411 loc) • 52.3 kB
Markdown
[](http://commitizen.github.io/cz-cli/)
## 概述
EWM(Enhanced-Wechat-Miniprogram的缩写) 是微信小程序原生开发插件,提供了新的实例构造器(DefineComponent),并加入了新的TS类型系统,让'小'程序拥有"大"能力。[gitee地址](https://gitee.com/panjinhry/ewm)
- **增强的实例构造器(DefineComponent)**
新的实例构建函数(DefineComponent)相比于原生构造器(Page/Component),字段逻辑更清晰,功能更齐全,类型更完善。
- **更规范的书写规则**
为了让代码逻辑更清晰,容易阅读,EWM加入了新的(很少的)配置字段和规则。例如,原生中methods字段下可以书写组件事件函数,内部方法 生命周期函数。EWM规则中,events字段书写组件事件函数,methods只书写内部方法,页面生命周期写在pageLifetimes字段下。这些规则是靠ts的类型来约束的。 如果您使用js开发,这些规则都是可选的。
- **独立的子组件**
当组件中包含多个子组件时,把所有组件数据和方法都写在一起很不方便阅读和维护,小程序提供的Behavior存在字段重复和隐式依赖等问题,这都可以认为是js的原罪。EWM 提供的子组件构建函数(CreateSubComponent) 配合 TS 类型系统解决以上问题,让复杂组件更易写易维护。
- **强大的类型系统**
EWM 拥有强大的类型推导系统,智能的字段提示、重复字段检测、类型检查,让错误被发现于书写代码时。
- **支持任何第三方组件**
当你引入第三方UI库组件(无组件类型)时,您只要为引入的组件书写一个组件类型(IComponentDoc),即可引入到EWM体系中。EWM提供了内置的泛型[CreateDoc](#createdoc),协助您书写第三方组件类型。
- **完美兼容**
EWM 提供的API和类型系统基于原生,所以不存在兼容性,想用即用。
- **对js友好**
虽然TS开发下更能发挥EWM的特性,但只要您熟悉了EWM规则,使用js开发也是不错的选择,EWM中很多运行时检测是专为js而写的。
## 安装
- **依赖安装(ts开发下)**
1. **typescript** `npm i --save-dev typescript@^4.6.0`
配置tsconfig.json
```json
{
"compilerOptions": {
//"lib": ["esnext"],最低es2015
"module": "ES6",
"strict": true,
"moduleResolution": "node",
"exactOptionalPropertyTypes": true
//...
}
}
```
2. **官方ts类型**
`npm i --save-dev @types/wechat-miniprogram`
- **安装 ewm**
1. npm安装: `npm i ewm`
2. 配置文件: ewm.config.js(书写在node_modules同级目录下,[配置规则](#iewmconfig))
```js
//内部默认配置
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如下
原文件
```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');
}
```
开发环境可更改为
```js
// node_modules\mobx\dist\index.js
module.exports = require('./mobx.cjs.development.js');
```
生产环境可更改为
```js
// node_modules\mobx\dist\index.js
module.exports = require('./mobx.cjs.development.js');
```
与EWM配置文件关联写法如下
```js
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 介绍](https://developers.weixin.qq.com/miniprogram/dev/devtools/npm.html)
> tips:更改配置文件后,需要重新npm构建并清除缓存后生效
## 思想
- **类型为先**
EWM 在设计各个配置字段或 API 时优先考虑的是能否契合TS的类型系统,这可能导致个别字段对于运行时来说是无意义的(生产环境会去掉)。因此相比js,使用ts开发更能发挥EWM的能力。比如 DefineComponent的path 字段,在js开发中可以忽略,但在ts开发下,此字段具有重要意义。
- **类型即文档**
EWM中,实例构建函数(DefineComponent)返回的类型好比传统意义的组件文档,为引用组件时提供类型支持。EWM内置了原生(Wm)组件类型(暂不完善),对于第三方ui库组件,EWM会逐步拓展,为其提供类型支持(欢迎您的PR)。组件类型书写简单,您完全可以为自己的项目书写[组件类型](#createdoc)。
**示例1**
> 示例中用到的类型可前往[重要类型](#重要类型)查看
```ts
// 自定义组件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**
```ts
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); // 跳过类型约束 不推荐
},
},
});
```
## 特色预览
- **[properties子字段支持书写任意类型](#properties)**
- **[状态管理(基于 mobx)](#responsive)**
- **[集成 computed 和 watch](#computed)**
- **[全实例注入](#inject)**
- **新的实例间传值方式**
1. **[子组件给父组件传值](#customevent)**
2. **[页面间传值](#publishevent)**
- **中文错误提示**
EWM在错误提示中加入了中文字段(在`⚠️`符号之间),方便快速找到错误原因。例如: `⚠️与注入的data字段重复⚠️`
> ⚠️有时TS会把错误标记到上级字段,实际为子字段报错! 解决报错应从上到下,由内而外,另外不符合EWM的tsconfig.json配置也可能导致类型错误。
## API
### MainData
> js开发可以忽略
书写复杂组件时,为了给单独书写的子组件模块提供主数据类型,需要将主数据抽离书写。
MainData函数只接受三个配置字段(properteis,data,computed)。
返回类型为IMainData:
```ts
interface IMainData {
properties?: Record<string, any>; //实际类型较复杂,这里简写了
data?: Record<string, any>; //实际类型较复杂,这里简写了
computed?: Record<string, any>; //实际类型较复杂,这里简写了
allMainData?: Record<string, any>; //实际类型较复杂,这里简写了
}
```
**示例 3**
```ts
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<a id="mainData" name="mainData">**
```ts
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 文档](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<a id="properties" name="properties"/>**
DefineComponent会根据此字段配置推导出具体类型,做为组件类型的一部分。
> ⚠️**组件类型严格区分必传和选传,辅助泛型[AuxType](#auxtype)**
1. **必传字段**
使用简写规则或**不带 value 字段的全写规则**(对象描述)。
**示例 5 简写必传字段**
```ts
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)效果和简写完全相同
```ts
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**
```ts
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<a id="responsive" name="responsive">**
新增 响应式数据字段(基于mobx)。
格式: "()=> observableObject.filed"
**示例 8**
```ts
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)。
```ts
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**
```ts
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** <a id="computed" name="computed">
> 同官方[miniprogram-computed](https://github.com/wechat-miniprogram/computed)
**示例 9**
```ts
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 <a id="customevent" name="customevent" />**
导入由CreateSubComponent建立的子模块,类型为:ISubComponent[]。
原生开发时,子组件给父组件传值通常使用实例上的 triggerEvent 方法.如下
**示例 10**
```ts
// sonComp.ts
import { DefineComponent } from 'ewm';
DefineComponent({
methods: {
onTap() {
// ...
this.triggerEvent('customEventA', 'hello world', {
bubbles: true,
composed: true,
capturePhase: true,
});
},
},
});
```
```html
<!-- parentComp.wxml -->
<sonComp bind:customEventA = "customEventA" />
```
```ts
// parentComp.ts
import { DefineComponent } from 'ewm';
DefineComponent({
methods: {
customEventA(e: WechatMiniprogram.CustomEvent) {
console.log(e.detail); // 'hello world'
},
},
});
```
> EWM写法
**示例 11**
```ts
// 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;
```
```html
<!-- parentComp.wxml -->
<view >
<sonComp bind:customEventA = "customEventA" bind:customEventB = "customEventB" />
</view>
```
**示例 12**
```ts
// 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**
```ts
// components/test/test
import { DefineComponent } from 'ewm';
// 构建组件
const customComponent = DefineComponent({
pageLifetimes: { // 组件下只开启3个字段
onShow() {
// ok
},
onHide() {
// ok
},
onResize() {
// ok
},
onLoad() {
// 报错 不支持的字段
},
onReady() {
// 报错 不支持的字段
},
},
});
```
**示例 14**
```ts
// 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 <a id='publishevent' name="publishevent"/>**
原生开发中当前页通过[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**
```ts
//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**
```ts
//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**
```ts
//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开发时可以如下书写<a id = "jspublish" name="jspublish">**
**示例 18**
```js
//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**
```js
//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**
```ts
//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**
```ts
//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 前缀规则**
```html
<!-- 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**
```ts
//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`,此字段不会被主组件继承,运行时会忽略此字段。
```html
<!-- /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 />
```
```ts
// 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**
暴漏给主逻辑调用的接口,主逻辑控制子模块的通道。前缀检测,重复字段检测
```ts
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
> **全实例注入类<a id="inject" name="inject">**
1. **书写注入文件**
```ts
// 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;
};
}
}
```
2. **导入注入文件**
```ts
// app.ts
import './path/inject';
App({});
```
3. **使用注入数据**
```ts
//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配置文件类型**
```ts
export interface IEwmConfig {
/**
* @default 'development'
* @description 生产环境会去掉运行时检测等功能。
*/
env?: 'development' | 'production';
/**
* @default 'ts'
* @description ts环境会关闭一些运行时检测。
*/
language?: 'ts' | 'js';
}
```
### CreateDoc
```ts
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的无私帮助
## 赞助


## ewm探讨群

若失效可在[官方论坛](https://developers.weixin.qq.com/community/develop/question)私信 Zhao ZW