
Research
Supply Chain Attack on Axios Pulls Malicious Dependency from npm
A supply chain attack on Axios introduced a malicious dependency, plain-crypto-js@4.2.1, published minutes earlier and absent from the project’s GitHub releases.
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开发下)
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
安装 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配置也可能导致类型错误。
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中最遗憾的地方,暂时还没有更佳的实现方案,期待您给与指点。
在EWM中实例(页面或组件)都是由DefineComponent函数构建的。 以下是对各个配置字段与原生规则不同之处的说明。在阅读说明前您可能需要了解官方 Component 文档。
path(新增)
js开发可忽略此字段。
构建页面实例时(TS)此字段为返回组件类型一部分,类型为/${string} 例如: path:"/pages/index/index"
运行时检测的报错信息:
[ ${组件路径} ] DefineComponent构建组件时,不应该书写path字段[ ${页面路径} ] DefineComponent构建页面时,应书写path字段,值为 /${页面路径}mainData(新增)
js开发可忽略此字段。
字段类型为IMainData,即MainData函数返回值,书写此字段后,不可再书写properties、data、computed字段(类型变为never)。
DefineComponent会根据此字段配置推导出具体类型,做为组件类型的一部分。
必传字段
使用简写规则或不带 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}; }
用于组件中包含多个子组件时,构建独立的子组件模块。
⚠️由于当前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;
书写注入文件
// 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
}
};
})
常用于辅助书写properties字段和customEvent字段类型
```ts
declare type AuxType<T = any> = {
new (...arg: any[]): T;
} | {
(): T;
};
```
EWM配置文件类型
export interface IEwmConfig {
/**
* @default 'development'
* @description 生产环境会去掉运行时检测等功能。
*/
env?: 'development' | 'production';
/**
* @default 'ts'
* @description ts环境会关闭一些运行时检测。
*/
language?: 'ts' | 'js';
}
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的无私帮助

若失效可在官方论坛私信 Zhao ZW
FAQs
The npm package ewm receives a total of 2 weekly downloads. As such, ewm popularity was classified as not popular.
We found that ewm demonstrated a not healthy version release cadence and project activity because the last version was released a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Research
A supply chain attack on Axios introduced a malicious dependency, plain-crypto-js@4.2.1, published minutes earlier and absent from the project’s GitHub releases.

Research
Malicious versions of the Telnyx Python SDK on PyPI delivered credential-stealing malware via a multi-stage supply chain attack.

Security News
TeamPCP is partnering with ransomware group Vect to turn open source supply chain attacks on tools like Trivy and LiteLLM into large-scale ransomware operations.