# ewm **Repository Path**: panjinhry/ewm ## Basic Information - **Project Name**: ewm - **Description**: 小程序原生开发插件 - **Primary Language**: TypeScript - **License**: MIT - **Default Branch**: develop - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2022-01-07 - **Last Updated**: 2023-04-27 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README [![Commitizen friendly](https://img.shields.io/badge/commitizen-friendly-brightgreen.svg)](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 { PropType, DefineComponent } from "ewm"; export interface User { name: string; age?: number; } const demoDoc = DefineComponent({ properties: { /** * @description num描述 */ num: Number, /** * @description str描述。 */ str: { type: String as PropType<"male" | "female">, value: "male", }, /** * @description union描述 */ union: { type: Array as PropType, value: { name: "zhao", age: 20 }, optionalTypes: [Object as PropType], }, }, customEvents: { // 字段书写规则请看 API——DefineComponent——customEvent。 /** * @description 自定义事件customeEventA描述 */ customeEventA: String as PropType<"male" | "female">, // detailType为string类型 => 'male' | 'female' /** * @description 自定义事件customeEventB描述 */ customeEventB: [String, Number], // detailType为联合类型 => string | number /** * @description 自定义事件customeEventC描述 */ customeEventC: { detailType: Object as PropType, // detailType为对象类型=> User options: { bubbles: true }, // 同原生写法 }, /** * @description 自定义事件customeEventD描述 */ customeEventD: { detailType: Array as unknown as PropType<[string, number]>, // detailType为元组类型 => [string,number] options: { bubbles: true, composed: true }, // 同原生写法 }, // ... }, // ... }); export type Demo = typeof demoDoc; // 导出组件类型 // Demo 等效于 // type Demo = { // properties: { // num: number; // str?: { // type: "male" | "female"; // default: "male"; // }; // union?: { // type: User | User[]; // default: { // name: "zhao"; // age: 20; // }; // }; // }; // events: { // customeEventA: 'male' | 'female'; // customeEventB: string | number; // customeEventC: { // detailType:{name: string; age?: number }, // options:{ bubbles: true } // }; // customeEventD: { // detailType:[string, number], // options:{ bubbles: true; composed: true } // }; // }; // }; ``` 示例1中导出的类型 Demo 好比如下书写的组件描述文档 | properties 属性 | 描述 | 默认值 | 类型 | 是否必传 | | :-----------: | :-----: | :----------------------: | :---------------: | :--: | | num | num描述 | | number | 是 | | str | str描述 | "male" | "male" \|"female" | 非 | | union | union描述 | { name: "zhao",age: 20 } | User \| User[] | 非 | | 自定义事件 | 描述 | 传递数据类型 | options 配置 | | :-----------: | :------------------: | :---------------------------: | :------------------------------: | | customeEventA | 自定义事件customeEventA描述 | 'male' \| 'female' | | | customeEventB | 自定义事件customeEventB描述 | string \| number | | | customeEventC | 自定义事件customeEventC描述 | {name: string, age?: number } | { bubbles: true } | | customeEventD | 自定义事件customeEventD描述 | [string, number] | { bubbles: true, composed: true } | - **关键数据和方法必须预声明** 原生开发时,子组件给父组件传值经常使用实例方法triggerEvent,这种写法不经意间把自定义事件名和配置隐藏在一些方法逻辑当中。不便重复调用,不易阅读,也无法导出类型。DefineComponent构建组件时中增加了customEvents字段用来书写自定义事件配置,方便代码阅读和类型导出。有些其他字段也基于此思想。例如DefineComponent构建页面时的publishEvents字段。 - **严格的数据管控** js开发或原生TS类型中,this.setData方法可以书写任何字段配置(或许data中原本没有声明的键名),不利于阅读,也不符合严格的单向数据流控制(组件应只能控制自身data字段),为避免造成数据混乱,EWM重写了setData的类型定义,要求输入配置时只能书写实例配置中data字段下(且非响应式字段)已定义的字段(除非使用as any 忽略TS类型检查),这也符合上面谈到思想————关键数据必须预声明。 **示例2** ```ts import { PropType, DefineComponent } from "ewm"; export interface User { name: string; age?: number; } DefineComponent({ properties: { str: String, user: Object as PropType, }, 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; // 实际类型较复杂,这里简写了 data?: Record; // 实际类型较复杂,这里简写了 computed?: Record; // 实际类型较复杂,这里简写了 allMainData?: Record; // 实际类型较复杂,这里简写了 } ``` **示例 3** ```ts import { PropType, DefineComponent } from "ewm"; interface User { name: string; age?: number; } const demoA = DefineComponent({ properties: { a: String, user: Object as PropType, }, data: { b: 123, }, computed: { name(data) { return data.user.name; }, }, }); export type DemoA = typeof demoA; ``` **示例 4** ```ts import { PropType, DefineComponent, MainData } from "ewm"; const mainData = MainData({ properties: { a: String, user: Object as PropType<{ name: string; age?: number }>, }, data: { b: 123, }, computed: { name(data) { return data.user.name; }, }, }); const demoB = DefineComponent({ mainData, // ... }); export type DemoB = typeof demoB; ``` DemoA和DemoB的类型完全一致,但在示例4中 主数据类型(typeof mainData)被单独提了出来,方便传递。 > 这是EWM中最遗憾的地方,暂时还没有更佳的实现方案,期待您给与指点。 ### DefineComponent 在EWM中实例(页面或组件)都是由DefineComponent函数构建的。 以下是对各个配置字段与原生规则不同之处的说明。在阅读说明前您可能需要了解官方 [Component 文档](https://developers.weixin.qq.com/miniprogram/dev/reference/api/Component.html)。 - **path(新增)** > js开发可忽略此字段。 构建页面实例时(TS)此字段为返回组件类型一部分,类型为`/${string}` 例如: `path:"/pages/index/index"` 运行时检测的报错信息: 1. 当构建组件时,书写了path字段: `[ ${组件路径} ] DefineComponent构建组件时,不应该书写path字段` 2. 当构建页面时 没有书写path字段或书写错误: `[ ${页面路径} ] DefineComponent构建页面时,应书写path字段,值为 /${页面路径}` - **mainData(新增)** > js开发可忽略此字段。 字段类型为IMainData,即[MainData](#mainData)函数返回值,书写此字段后,不可再书写properties、data、computed字段(类型变为never)。 - **properties** DefineComponent会根据此字段配置推导出具体类型,做为组件类型的一部分。 > ⚠️**组件类型严格区分必传和选传,辅助泛型[PropType](#PropType)** 1. **必传字段** 使用简写规则或**不带 value 字段的全写规则**(对象描述)。 **示例 5 简写必传字段** ```ts import { PropType, DefineComponent } from "ewm"; export interface User { name: string; age?: number; } export interface Cart { goodsName: string[]; count: number; } const demoDoc = DefineComponent({ properties: { str: String, // => string 简写 strUnion: String as PropType<"red" | "black" | "white">, // => 'red'|'black'|'white' num: Number, // => number numUnion: Number as PropType<100 | 200 | 300>, // => 100 | 200 | 300 bool: Boolean, // => boolean arr: Array, // => unknown[] 不推荐写法,描述过于宽泛 arrUnion: Array as PropType<(string | number)[]>, // => (string|number)[] obj: Object, // => Record 不推荐写法,描述过于宽泛 objUnion: Object as PropType, // => User | Cart tuple: Array as unknown as PropType<[Cart, User]>, // => [User,Cart] 唯一需要使用as unknown 的地方, }, }); export type DemoDoc = typeof demoDoc; // Demo1Doc的类型相当于 // type DemoDoc = { // properties: { // str: string; // num: number; // bool: boolean; // strUnion: "red" | "black" | "white"; // numUnion: 100 | 200 | 300; // arr: unknown[]; // obj: {[x: string]: any}; // arrUnion: (string | number)[]; // objUnion: { // name: string; // age?: number; // } | { // goodsName: string[]; // count: number; // }; // tuple: [{ // goodsName: string[]; // count: number; // }, { // name: string; // age?: number; // }]; // }; // } ``` > ⚠️ 简写字段中的联合类型描述只限于同类型的联合, 比如 `"red" | "black"` 或 `100 | 200` 或`string[] | number[]`或 `User | Cart` 都是同一原始类型的联合类型, 不同原始类型的联合(string|number)见示例 6。**元组类型是唯一需要使用 as unknown 转译的**。 **示例 6 全写必传属性** 当字段类型为不同原始类型的联合类型时,使用全写规则 全写规则下如果只写 type 字段(无 value 和 optionalTypes)效果和简写完全相同 ```ts import { DefineComponent, PropType } from "ewm"; export interface User { name: string; age?: number; } export interface Cart { goodsName:string[] count:number } const demoDoc = DefineComponent({ str: { type: String }, strUnion: { type: String as PropType<'red' | 'black' | 'white'> }, num: { type: Number }, numUnion: { type: Number as PropType<100 | 200 | 300> }, bool: { type: Boolean }, arr: { type: Array }, arrUnion: { type: Array as PropType<(string | number)[]> }, obj: { type: Object }, objUnion: { type: Object as PropType }, tuple: { type: Array as unknown as PropType<[Cart, User]> }, //以上就是示例5中必传字段的全写描述,效果同示例5的简写完全相同 //以下是不同原始类型的联合写法 str_number:{ type:String,optionalTypes:[Number] } // => string | number arr_obj: { type:Array as PropType,optionalTypes:[Object as PropType]} // => 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 { PropType, DefineComponent } from "ewm"; export interface User { name: string; age?: number; } export interface Cart { goodsName: string[]; count: number; } const demoDoc = DefineComponent({ properties: { num: { type: Number, value: 123 }, // => { num?:{ type:number, default:123} } errorNum: { type: Number, value: "123" }, // => error `Type 'string' is not assignable to type 'never'.` str: { type: String, value: "123" }, // => { str?: { type:string, default:'123'} } bool: { type: Boolean, value: false }, // => { bool?: { type:boolean, default:false} } arr: { type: Array as PropType, value: [1, 2, 3], }, // =>{ arr?:{type:number[],default:[1,2,3] } } obj: { type: Object as PropType, value: { name: "zhao" }, }, // => { obj?: {type:User,default:{ name: "zhao" }} } union: { type: Number, value: "string", // ok optionalTypes: [String, Object], }, // => { union?: { type: string | number | object; default: "string" } } union1: { type: Boolean, value: { name: "zhao" }, // ok optionalTypes: [ Array as PropType, Object as PropType, ], }, // { union1?: { type: boolean | Cart[] | User, default: {name:'zhao'}} } union2: { type: String as PropType<"a" | "b" | "c">, value: 123, optionalTypes: [ Number as PropType<123 | 456>, Array as PropType, Boolean, Object as PropType, ], }, // {union2?: { type: 'a'|'b'|'c'| 123 | 456 | string[] | number[] | boolean | Cart | User; default: 123 }} }, }); export type DemoDoc = typeof demoDoc; ``` - **data** 新增 响应式数据字段(基于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** > 同官方[miniprogram-computed](https://github.com/wechat-miniprogram/computed) **示例 9** ```ts import { PropType, DefineComponent } from "ewm"; import { observable, runInAction } from "mobx"; interface User { name: string; age: number; } interface Cart { count: number; averagePrice: number; } const store = observable({ cart: { count: 0, averagePrice: 10 }, }); DefineComponent({ properties: { str: { type: String as PropType<"male" | "female">, }, user: { type: Object as PropType, 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** ```ts // sonComp.ts import { DefineComponent } from "ewm"; DefineComponent({ methods: { onTap() { // ... this.triggerEvent("customEventA", "hello world", { bubbles: true, composed: true, capturePhase: true, }); }, }, }); ``` ```html ``` ```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 PropType, options: { bubbles: true }, }, customEventC: { detailType: [Array as PropType, 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 ``` **示例 12** ```ts // Components/Parent/Parent.ts import { Sub } from "Components/subComp/subComp"; import { CreateSubComponent, DefineComponent } from "ewm"; const subComp = CreateSubComponent<{}, Sub>()({ // ...子组件数据和方法 }); const parentDoc = DefineComponent({ subComponent: [subComp], // 通过subComponent字段引入子组件(类型) events: { customEventA(e) { // e => WechatMiniprogram.CustomEvent console.log(e.detail); // => 'hello world' }, customEventB(e) { console.log(e.detail); // => ['1','2','3'] }, customEventC(e) { console.log(e.detail); // => 'string' , ['a','b','c'] }, }, }); export type Parent = typeof parentDoc; // Parent 等效于 { customEventC: { detailType:string | string[],options:{ bubbles: true, composed: true }} 因为Sub中定义的customEventC事件是冒泡并穿透的,Parent会继承类型。 ``` > 小结: 组件间传值时子组件应该把自定义事件配置定义在customEvents字段中。父组件会在events字段中得到子组件的自定义事件类型。 - **events** > 组件事件函数字段(包含子组件自定义事件)。 > 类型: `{[k :string]:(e:WechatMiniprogram.BaseEvent)=>void }` > ⚠️内部自动导入 subComponent字段中的子组件事件类型,方便获取代码提示。 > events字段类型没有加入到this上,因为events是系统事件。 - **pageLifetimes** 原生中小程序使用Component构建组件时,pageLifetimes子字段为:show、hide、resize,EWM拓展为同页面生命周期一样字段 onHide、onShow、onResiz。 原生中小程序使用Component构建页面时,要求把页面生命周期写在methods下, EWM改为还写在pageLifetimes字段中。 > 小结: EWM页面生命周期永远写在pageLifetimes下,组件实例中只提示3个字段(onHide、onShow、onResiz),页面实例提示全周期字段。js开发下此规则可选。 > **示例 13** ```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 ** 原生开发中当前页通过[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 url: "/pages/PageA/PageA", data: { fromPageUrl: this.is }, // 支持传递特殊字符 ; / ? : @ & = + $ , # }).then((res) => { console.log(res.errMsg); // "navigateTo:ok " 打印顺序 1 }); }, }, }); ``` **示例 16** ```ts // pages/PageA/PageA.ts import { PropType, DefineComponent } from "ewm"; import { PageB } from "../PageB/PageB"; const pageADoc = DefineComponent({ path: "/pages/PageA/PageA", properties: { // 定义页面接收的数据类型,与组件不同之处在于非响应式,即页面只在onLoad时接收传值。 fromPageUrl: String, }, publishEvents: { // 定义一个发布事件,事件名 publishA 参数为string publishA: String, }, subscribeEvents(h) { // 订阅事件字段 return h({ "/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页面 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 { PropType, DefineComponent } from "ewm"; const pageBDoc = DefineComponent({ path: "/pages/PageB/PageB", properties: { fromPageUrl: String, }, publishEvents: { // 发布事件名 publishB,联合类型写成数组形式 publishB: [String, Array as PropType], // 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** ```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