enhanced-miniprogram v3.0.8-beta3
enhanced-miniprogram {ignore=true}
概述
- 微信小程序原生开发增强插件
- 规范组件数据结构用以复杂项目
- 严格的类型检查(TS)
安装
- 在小程序目录下执行
npm i enhanced-miniprogram -S
- 构建 npm
增强功能简介
支持响应式数据(基于mobx)
格式: ()=> observableObject.filed
import { EPage } from "EMP";
import { observable } from "mobx"; //默认为mobx6
const user = observable({
name: "zhao",
age: 20
});
setTimeout(() => {
user.name = "liu";
user.age++;
}, 1000);
EPage({
data: {
name: user.name, //name字段非响应式写法,不具备响应式
age: () => user.age //age字段具有响应式 即当user.age改变时,实例自动更新age为最新的user.age
},
onload() {
console.log(this.data.name, this.data.age); //"zhao",20
setTimeout(() => {
console.log(this.data.name, this.data.age); //"zhao" ,21
}, 1000);
}
});
集成 computed 和 watch
(同 miniprogram-computed 项目地址)
import { EPage, EComponent } from "Enhanced-miniprogram";
interface User {
name: string;
age: number;
}
EPage({
data: {
user: <User>{ name: "zhao", age: 20 }
},
computed: {
age(data) {
return data.user.age + 1; //age:21
}
},
watch: {
"age,user"(valueAge: number, valueUser: User) {},
"user.**"(newUser) {}, //自动推导newUser的类型为User
"user.name"(newName) {}, //自动推导newName的类型为string
age(newAge) {}, //自动推导newAge的类型为number
user(newUser) {} //自动推导newUser的类型为User
}
});
全局依赖注入
//inject.js
import { EPage, EComponent } from "EMP";
//待注入的数据
const globalData = { user: { name: "zhao", age: 20 } };
const themeStore = observable({
theme: wx.getSystemInfoSync().theme
});
wx.onThemeChange((Res) => {
themeStore.theme = Res.theme;
});
//待注入的方法
function print(data:string){
console.log(data)
}
//注入
GlobalInject.inject({
data: {
theme: () => themeStore.theme,
globalData:globalData
},
options: {
addGlobalClass: true,
multipleSlots: true,
pureDataPattern: /^_/
},
methods:{
print,
}
})
//ts开发需写入注入类型如下
// declare module 'EMA' {
// interface GlobalInjectOptions {
// data: {
// theme:"dark" | "light" | undefined,
// name:string
// }
// }
// }
tips 如果您有更好的导入类型方式,PR
严格的类型检查
字段限制和约束 很多地方都加入了字段限制,比如 Page 配置对象除了规定的字段,不可有额外字段。不可向原生那样方法直接写在配置中,应写在 methods 字段下。 properties 中检查字段,写错,写多都不可以。 即使是继承父类型的子类型也加了字段溢出检查。子类型字段不可以超出继承的父类型字段。 computed 的 data,watch 的字段,this 实例,所有能检查的地方都有严格的类型检查。
绝对的单向数据流。 例如 this.setData 方法已经重写了类型,约束只能对 data 字段进行 setData,无法对 properties 字段、计算字段、注入字段、behavior 字段、响应式字段 setData, 除非你放弃 ts 的类型检查。
核心思想
组件 TS 类型代替组件文档
在定义自定义组件的时候,把与外部的关联通过类型声明导出(export),即用 ts 类型描述组件文档。在使用组件的时,导入要使用的组件类型(import),就清楚组件的使用规则。这样做的好处是,定义者省得写文档,使用者免去查阅,通过导出类型,即可以明白如何使用组件。
**示例代码1 创建自定义组件sayHello**
```ts
//components/sayHello.wxml
<view bindtap="onTap" style="color:{{color}}">按钮<view>
//components/sayHello.ts
import {EComponent} from 'Enhanced-miniprogram'
interface User {name:string,age?:number} //or class User{...}
const propertiesType = EComponent({
properties:{
/*
按钮颜色
*/
color:{
type:String,
value:<const>'black' //value必须使用断言 表示默认值
},
/*
做为triggerEvent的detail参数
*/
user:{
type:Object as PropType<User>, //可使用PropType类型断言具体类型 or class User
value:<User>{}
},
/*
组件被点击时触发回调triggerEvent事件
*/
customEventA:{ //定义自定义事件名称(做为原生triggerEvent事件的第一个参数)
type:'customEvent', //其实没什么用,主要为了一目了然的看到哪个是自定义事件字段。
detailType:String, //定义原生triggerEvent的第二个参数类型
// options?:{bubbles?: boolean,composed?: boolean,capturePhase?: boolean} 将做为原生triggerEvent的第三个参数
}
},
methods:{
onTap(){
const name = this.data.user.name;
this.customEventA(`欢迎您:${name}`) //好比this.triggerEvent('customEventA',`欢迎您:${name}`,options: 如果上面customEventA中有写options字段,将赋值到这里)
}
}
});
type DOC = typeof propertiesType
export {DOC,User} //导出组件外部属性,和内部类型,方便使用者。
/**导出的类型为
* type SayHello = {
properties: {
color?: { //带?表示非必传,
type:string, //类型为string
default:"black" //默认值为'black'
};
user: { //不带?表示必传,类型为{name:string,age?:number}即User
name: string;
age?: number;
};
};
customEvent: { //自定义事件字段
customEventA: (detail: string) => void; //告诉调用者,此组件有一个名为customEventA的 自定义事件(tiggerEvent事件),类型为(detail: string) => void
};
}
*/
```
tips 除了基本类型外,通过 propType可实现 所有 ts 类型 (联合类型,元组类型,自定义对象 数组...)
例如 Properties:{
classA:Class User, //自定义构造函数约束类型。
classB:{type:Class User,value:<User>{}
}
}
示例代码 2 使用自定义组件 sayHello
//page/index.json
{
"usingComponents": {
"sayHello":"/components/sayHello"
}
}
//Page/index.wxml
<view>
<sayHello bind:customEventA="sayHelloCB" />
<view>
//page/index.ts
import {EPage,PageSubData} from "enhanced-miniprogram"
import * as SayHello from '/components/SayHello'
const subCompData = PageSubData<SayHello.Doc>()({//在组件中使用ComponentSubData
data:{
color:'red', // 可以不写,因为不是必传字段。
user:{name:'zhao'}, //必传字段,且类型必须为SayHello.User
otherField:'错误的字段报错'// 非SayHello类型字段,是要报错的。
}
})
EPage({ //or EComponent
subComponent:[subCompData], //增加的字段 用于导入子组件数据 可以导入多个
subCustomEvent:{ //当subComponent中有customEvent字段时,可在此定义接收函数
sayHelloCB(detail,e){ //ok detail类型为string,e为事件参数,detail === e.detail true
console.log(detail,e)
},
unknownField(detail,e){//报错,因为ts类型检测不到subComponent中传入的子组件类型数据中有此自定义事件字段
}
}
})
更好的细粒度开发
示例
const subDataA = ComponentSubData<A.Doc>()({...})
const subDataB = ComponentSubData<B.Doc>()({...})
const subDataC = ComponentSubData<C.Doc>()({...})
const subDataD = ComponentSubData<D.Doc>()({//假设D类型为{subName:string,subAge:number,subData:string}
properties:{
subData:String //可以继续向上传递需求,好比这写在父组件中,数据实际上是爷爷的。
},
data:{
subAge:10 //给D.Doc中的数据赋值。
},
computed:{
subName(data){
return data.obj.name //这里是把父组件的obj数据拿过来给子组件name赋值。
}
},
methods:{
changeSubAge(newAge){ //只能通过自己的方法改变自身数据
this.setData({
subAge:newAge
})
}
}
})
interface User = {name:string,age:number}
const prop = Ecomponent({
properties:{
obj:Object as PropType<User>
}
data(){
return {
name:this.properties.obj.name
}
},
watch:{
age(newAgeValue){
this.changeSubAge(newAgeValue)//向改变子组件数据,只能通过调用子组件方法实现,这里直接setData是不可以的。除非你放弃类型约束。
}
}
subComponent:[subDataA,subDataB,subDataC,subDataD]//所有的子类型会注入进来。
})
export {prop,User} //注意这个prop中不仅有自身的obj属性,还有子组件向爷爷要的数据subData。
在上面的示例中,可以看出,复杂的页面或者组件引用其他自定义组件时,需要单独建立子组件数据,然后注入到当前页面或组件实例中。
每个子组件的数据是单独存放的,也是自身单独控制的。父组件想要调用更改子组件数据,只能通过调用子组件的方法来修改。
插件提供的 PageSubData 和 CompSubData 基于原生的 Behavior 函数,虽然如此,但插件提供了更严格的书写方式和类型检查。让基于 Behavior 书写子组件数据变为现实。
无所不在的类型检查
小程序提供的 ts 类型库(@types/wechat-miniprogram)只给了一些基础类型提示,且更新速度缓慢。 这也是开发此插件的根本原因之一,enhanced-miniprogram 提供了有些"变态"的类型检查。这里简单描述,后面有详细的阐述。
重复字段检查 在书写一个组件或页面实例配置的时,如果 properties,data,computed,behaviors 中引入的数据,有重复字段,就会报错(不能将类型 xxx 分配给类型“never”)。
interface User { name: string; age: number; } EComponent({ properties: { age: Number, user: { type: Object, value: <User>{} } }, computed: { age(data) { //报错:“不能将类型 number 分配给类型“never” 因为properties中有age字段了 return data.user.age; } } });
字段拼写检查,无效字段检查,多余字段检查
类型错误检查...
基于原生的好处
- 完美兼容 当你用 EPage 或 EComponent 配置实例发现一些插件错误的时候,完全可以转向用原生 Page 或 Component 配置实例,提交 issue,待修复 bug 后再用 EPage 或 EComponent 替换原生配置。
- 渐进融合 如何你喜欢这种开发方式,你可以渐进式的把过去的代码逐步替换。简单的复制粘贴即可,不必大量修改代码。
功能详情
EComponent
- properties
书写规则
简写字段 值类型:String | Number | Boolean | Object | Array 返回类型:'' | 0 | false | { k:string : any } | unknown[]
properties:{ str:String, num:Number, bool:Boolean, arr:Array, obj:Object }
对象字段
值类型:{ type:String | Number | Boolean | Object | Array, required?:boolean //不写 默认为 false value?:'use assert' } 返回类型:value 字段类型(应使用断言),若未写 value 字段,返回对应 type 值的类型。
tips 类型断言可以让使用者通过类型看到组件字段的默认值,应在 value 值为 string | number | [] | {} 的情况时使用断言
interface User{ id:string,age:number} properties:{ str:{ type:String, value:<const>'annil' //返回类型为'annil' 若不写此字段返回类型为'',若不断言返回类型为string,推荐使用断言 } num:{ type:Number, required:true, //不写默认为 false value:<const>100 //返回类型为100 若不写此字段返回类型为0,若不断言返回类型为number,推荐使用断言 } bool:{ type:Boolean, required:true, value:true //返回类型为true 若不写此字段返回类型为false,boolean类型不必断言 } arr:{ type:Array, required:true, value:[1,2,3] //返回类型为number[] 若不写此字段返回类型为unknown[],value值不为空[],可不使用断言,若为[] 应使用断言 (value:<number[]>[]) }{ [k:string] : any } obj:{ type:Object, required:true, value:<User>{} //返回类型为User 若不写此字段返回类型为{ [k:string] : any },使用断言因为value值为空{} } }
自定义事件字段(类似原生实例中的 triggerEvent)
值类型:{ type: 'customEvent', detailType: String | Number |Boolean | {k:string:any}, options?: { bubbles?: boolean,composed?: boolean,capturePhase?: boolean} }
返回类型:{ customEvent:{ fieldA:(detail:对应 detailType,options:对应 options)=>void } }
tips 此字段是为了让调用者清楚组件向外部 trigger 的自定义事件,detailType 字段定义原生实例 triggerEvent 函数的第二个属性的类型,options 做为原生实例 triggerEvent 函数的第三个参数值,不写子字段默认全为 false。此字段会在实例中建立一个方法,可直接调用,看下方示例。
import { EComponent } from "Enhanced-miniprogram"; interface DemoUser { name: string; age: number; } EComponent({ properties: { customEventA: { type: "customEvent", detailType: <DemoUser>{} }, customEventB: { type: "customEvent", detailType: <DemoUser>{}, options: { //不写此字段,子字段默认为空对象,即子字段值全为 false bubbles: true, composed: true, capturePhase: true } } }, methods: { onTap() { this.customEventA({ name: "annil", age: 22 }); //相当于原生: this.triggerEvent('customEventA',{name: 'annil',age: 22}) this.customEventB({ name: "annil", age: 22 }); //相当于原生: this.triggerEvent('customEventB',{name: 'annil',age: 22},options: { bubbles: true,composed: true, capturePhase: true }) } } });
字段检测
js 运行时检测
import { EComponent } from "Enhanced-miniprogram"; EComponent({ properties:{ fieldD:{ type:String, require:true, //报错 require 少s了 values:'string', //报错 values 多s了 otherField:'xxx' //报错 多余的字段 } customEventFoo:{ type: 'customEvent', detailType: String options?: { bubble?: boolean,compose?: boolean,capturePhase?: boolean},//自定义事件全字段有检测 otherField:'xxx' //报错 多余的字段 } } })
总结示例
```ts // component/demo.ts import { EComponent } from "Enhanced-miniprogram"; export interface DemoUser { name: string; age: number; } const demo = EComponent({ properties: { str: String, num: { type: Number, value: <const>100 }, bool: { type: Boolean }, arr: { type: Array, required: false, value: <DemoUser[]>[] }, obj: { type: Object, required: true, value: <DemoUser>{} }, customEventFoo: { type: 'customEvent', detailType:<DemoUser>{} , options: { //不写此字段,子字段默认为空对象,即子字段值全为false bubbles: true, composed: true, capturePhase: true } } }, methods: { onTap() { //调用customEvent事件 this.customEventFoo({name: 'annil',age: 22}); //相当于原生: this.triggerEvent('customEventFoo',{name: 'annil',age: 22},options=properties中定义的options) } } }); export type Demo = keyof demo /** Demo 类型等于 type Demo = { properties: { str?: ""; obj: { name: string; age: number; }; num?: 100; bool?: false; arr?: { name: string; age: number; }[]; }; customEvent: { customEventFoo: (detail: DemoUser, options: { bubbles: true; composed: true; capturePhase: true; }) => void; }; ```
- data
字段重名检测
{ properties:{ strA:String, }, data:{ strB:'' //ok strA:'' //error 不能将类型“string”分配给类型“never” } }
- computed
书写示例
import { EComponent } from "Enhanced-miniprogram"; interface User {name:string,age:number} EComponent({ properties:{ objInProperty:{ type:Object, value:<User>{} } }, data:{ objInData:{name:'zhao'} } computed:{ filedAge(data){ return data.objInProperty.age }, filedName(data){ return data.objInData.name }, fieldInject(data){//参数data中包含注入的数据 return data.injectObj.xxx } } })
类型约束
- 必须有返回值
- 字段名不能与 properties 和 data 中相同
- data 数据为只读
- methods
- 类型约束
- 不能与 proerties 中的 customEvent 类型字段重名
- 不能定义子组件 customEvent 函数(应定义在 customEvent 字段中)
- customEvent
- 字段描述 新增字段,为应对复杂组件中清晰子组件自定义事件函数位置。 此字段涉及子组件嵌套,建议看完 CompSubData 函数再看。
书写示例
import { EComponent,CompSubData } from "Enhanced-miniprogram"; import {subCompA} from '../component/subCompA' import {subCompB} from '../component/subCompB' const subAData = CompSubData<subCompA>(){ properties:{ //... } } const subBData = CompSubData<subCompB>(){ data:{ //... } } EComponent({ customEvent:{ subAcustomEvent(detail,e){ //detial为子组件传递的数据,e为事件对象 console.log(detial === e.detail)//true }, subBcustomEvent(detail,e){ console.log(detial === e.detail)//true } }, behaviors:[subAData,subBData] })
- watch
- 字段描述 集成 computed 和 watch
- 类型约束 字段只能包含 data 和 properites 和 computed 中的字段
- behaviors
- 字段描述 同原生,负责传递子组件数据和类型(子组件自定义事件类型也由此传入)
- 类型约束 类型为 IBehaviors[]
更细粒度书写复杂组件 (基于原生 Behavior)
先看示例 (后面有解释)
//页面中引入component/demo.ts组件和component/demo1.ts组件 import { DemoCompDoc } from "component/demo.ts"; import { DemoCompDoc } from "component/demo1.ts"; const mainData =
const subDemo = CreateSubCompData(DemoCompDoc, {
data:{
str:'zhao'
},
computed:{
arr(data){
return data.
}
}
},mainData);
const sub1 = CreateSubCompData(otherCompDoc1, options);//省略
EPage({
behavoirs: [subDemo,sub1, sub2, sub3]
});
```
复杂情况下,每个子组件之间可能要有数据交互影响,这个时候需要一个主数据,对主数据有需求的子组件可以引入主数据,(实际上这么做只是为了子组件可以有严格的类型检查)。
如下
```ts
import { xxxCompDoc1,xxxCompDoc2,xxxCompDoc3} from "path";
const mainData = CreateMainData({ //只能写数据。
properties:{},//爷爷实例传递过来的数据,需要满足子组件提交的properties
data:{},
computed:{}
})
const sub1 = CreateSubCompData(xxxCompDoc1,{
properties:{}, //把由爷爷决定的数据提交上层
computed:{},//可由mainData数据得到计算属性。
watch:{}
methods:{}, //事件只能修改自身组件数据。
lifetimes:{
attached(){
//this实例只有自身数据和方法
}
}
},mainData)
const sub2 = CreateSubCompData(xxxCompDoc2,options,mainData)
const sub3 = CreateSubCompData(xxxCompDoc3,options,mainData)
EComponent({
behavoirs:[mainData,sub1,sub2,sub3],
customEvent:{} //写所有子组件triggerEvent事件。
methods:{
//负责整体数据逻辑, 自能修改mainData数据,通过调用各个子组件方法,修改子组件数据。
//比如当sub1中trigger一个事件时候T,T事件调用sub2中的事件修改sub2的方法。
},
lifetimes:{
attached(){
//this实例只有main数据 、自身和子组件方法
}
}
})
```
使用示例
- 公共数据的注入 (EPage 中默认注入了当前主题的响应式数据 theme)
//inject.js
import {EPage,EComponent} from 'enhanced-miniprogram'
//创建响应式数据theme 基于mobx5(proxy)
const objStore = observable({
count:0
})
setInterval(() => {
objStore.count++
}, 1000)
EPage.inject({
options: {
multipleSlots:true,
pureDataPattern:/^_/,
//...
},
data: {
//加入globalData数据到每一个页面实例(非响应式)
globalData: {
name:'zhao',
//...
},
//响应式数据(格式为函数返回值形式), themeStore.theme发生变换时,自动更新页面数据(即自动setData)
count:()=>objStore.count
}
methods: {
print(data){ //加入公共方法到每一个页面实例
console.log(data)
}
}
//ts部分(如使用ts开发,需要按下面写入IInjectPage类型和IInjectComponent,这样可在每个实例下获取注入的数据类型,有更好的方式欢迎提醒)
declare global {
interface IInjectPage { //需要注意这里必须是此名称不然无效,不写options类型,因为不需要,无效。
data: {
globalData:{
name:number,
},
count:number
//...
},
methods:{
/** 实例调用时会显示注解
* @param value 打印输入的字符串
*/
print:(data:string)=>void
}
}
interface IInjectComponent {//需要注意这里必须是此名称不然无效
data: {
//...
}
methods: {
//...
}
}
}
// App.js 引入inject.js
import "./inject"
App({})
// xxx.ts
import {EPage} from 'enhanced-miniprogram'
EPage({
onload(){
console.log(this.data) //{globalData:{name:string},theme:Theme,count:number}
this.print('hello world') // 'hello world'
}
})
- EPage 的基本使用
//tabbar.ts
import { EPage } from "enhanced-miniprogram";
const User = observable({
name: "zhao",
age: 20
});
setInterval(() => {
User.age++;
}, 1000);
class Cart {
@observable public goodsName = "苹果";
@observable public price = 20;
}
const cart = new Cart();
setInterval(() => {
cart.price++;
}, 1000);
EPage(
{
data: {
UserName: User.name, //不具有响应式
UserAge: () => User.age, // 响应式数据
cartGoodsName:cart.goodsName //不具有响应式
cartPrice:()=>cart.price //响应式数据
},
computed: {
//同miniprogram-computed 见注释
},
watch: {
//同miniprogram-computed 见注释
},
lifetimes: {
attached() {
//this中可获取注入的方法,this.data中可获取注入的数据
}
}
},
"tabbar"
); //可选属性,建议同组件名,看CreateComponentData部分的解释
- CreateComponentData 当在组件或页面中引入子组件的时,可按如下书写
import {tabbar} from './tabbar.ts'
import { CreateComponentData,EPage } from "enhanced-miniprogram";
//CreateComponentData,创建behavior数据 一参为要创建的组件的文档,
// const tabbarData = CreateComponentData(tabbar,propertyData)
//propertyData中data对象绑定非响应式数据,bindData中绑定响应式数据,customEvent绑定函数。(ts开发,会有严格的类型提示,必选字段必须有)
//字段有前缀是因为在创建tabbar组件时,写入了第二个参数
const tabbarData = CreateComponentData(tabbar,{
data:{
tabbar_dataA:Number,//可选属性可不传
},
bindData:{
tabbar_dataB:()=>xxx.field //必选属性必须有
},
customEvent:{ //函数运行时,会传入2个参数,1参为子组件调用此函数时的一参,二参为子组件调用此函数时的事件函数
tabbar_funcA(detial:any,e:WechatMiniprogram.customEvent){}, //可选函数
tabbar_funcB(detial:string,e:WechatMiniprogram.customEvent){} //必选函数
}
})
EPage({
behaviors:[tabbarData] //通过behaviors把多个子组件数据聚合在一起
onLoad(){
this.tabbar_funcB('string') //自身调用传递给子组件的函数 参数类型跟随在createComponentData时的定义类型。
}
})
// EComponent({ 使用方法同上
// behaviors:[tabbarData]
// })
- CreateNativeComponentData
参与贡献
- Fork 本仓库
- 新建 Feat_xxx 分支
- 提交代码
- 新建 Pull Request // EComponent 一参同 Component,二参为可选字符串,避免命名冲突。 properties: { dataA: Number, dataB: { type: String, required: true // ts 特性 不写默认 false }, funcA: Function, // ts 特性 简写默认为可选传入属性 funcB: { type: Function, required: true } }, bindData: { //再次强调,响应式数据格式必须为函数返回,且为可观察对象,否则值非自动更新,建议使用 mobx reactiveDataA: () => storeA.fieldX, reactiveDataB: () => storeB.fieldY },
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago