From 03f64523dc5f7da32616ec5c0ecfc4f50d4d4b7d Mon Sep 17 00:00:00 2001 From: missanni Date: Mon, 25 Dec 2023 20:04:58 +0800 Subject: [PATCH] fix: isPagecheck --- README.md | 41 ++- doc/api/DefineComponent.md | 20 +- doc/api/RootComponent.md | 166 +++++++-- doc/api/SubComponent.md | 129 +++++++ doc/api/navigateTo.md | 66 ++++ doc/designIdea.md | 324 ++++++++++++++++++ doc/fields/computed.md | 93 ----- doc/fields/store.md | 59 ---- doc/fields/watch.md | 99 ------ doc/standard.md | 106 ------ jest.config.ts | 9 +- jest/computed/normal/normal.ts | 4 + .../collectOptionsForComponent.ts | 71 ++-- .../RootComponent/Data/test/normal.test.ts | 2 +- .../RootComponent/Events/test/normal.test.ts | 4 +- .../Lifetimes/LifetimesOption.ts | 61 ++-- .../Lifetimes/test/error.test.ts | 8 +- .../RootComponent/Methods/test/mormal.test.ts | 2 +- .../PageLifetimes/PageLifetimesOption.ts | 5 +- .../PageLifetimes/test/normal.test.ts | 17 +- src/api/RootComponent/RootComponentDoc.ts | 32 +- .../Watch/test/WtachInject.test.ts | 2 +- src/api/RootComponent/index.ts | 4 +- .../SubComponent/SubData/SubDataConstraint.ts | 2 +- .../SubInherit/SubInheritConstraint.ts | 2 +- .../SubInstance/test/normal.test.ts | 4 - .../SubLifetimes/SubLifetimesOption.ts | 4 +- ...ataConstraint.ts => SubStoreConstraint.ts} | 0 .../SubComponent/SubStore/SubStoreOption.ts | 5 +- .../SubComponent/SubStore/test/error.test.ts | 4 +- .../SubComponent/SubStore/test/normal.test.ts | 14 +- .../SubWatch/test/WatchProperties.test.ts | 161 +++++++++ src/api/SubComponent/index.ts | 7 +- src/behaviors/BComputedAndWatch/isEqual.ts | 3 +- 34 files changed, 997 insertions(+), 533 deletions(-) create mode 100644 doc/api/navigateTo.md create mode 100644 doc/designIdea.md delete mode 100644 doc/fields/computed.md delete mode 100644 doc/fields/store.md delete mode 100644 doc/fields/watch.md delete mode 100644 doc/standard.md rename src/api/SubComponent/SubStore/{SubDataConstraint.ts => SubStoreConstraint.ts} (100%) create mode 100644 src/api/SubComponent/SubWatch/test/WatchProperties.test.ts diff --git a/README.md b/README.md index dac7828..7038172 100644 --- a/README.md +++ b/README.md @@ -23,38 +23,40 @@ ### 简介 -annil(安奈儿)是微信小程序原生开发插件。 +annil(安奈儿)是微信小程序原生开发插件,相较于原生API,使用annil会大大提高开发效率和代码质量。 ### 特点 -- **新的组件构建API** +- **组件构建API功能更强大** 新的组件构建API加入了[computed](./doc/fields/computed.md)、[watch](./doc/fields/watch.md)、[store](./doc/fields/store.md)(基于mobx的全局响应式数据)`等功能,使开发更便捷。 - **复杂组件解决方案** - 新的组件构建方案(根组件 + 子组件(可选)),组件逻辑与wxml元素(标签)一一对应。解决书写复杂组件时代码逻辑耦合的问题。 + 新的组件构建方案(根组件 + 子组件(可选)),组件逻辑与wxml标签一一对应。解决书写复杂组件时不同组件代码耦合的问题。 +- **专为typescript开发设计** + + annil是专为TS开发而设计的,通过类型提示、约束、检测,提升开发效率,避免运行时错误的发生。使用js开发时可忽略开发文档中的ts部分。 - **组件类型概念** - 在使用ts开发时,新的组件构建API返回的类型叫组件文档类型,好比传统组件(UI)库为每个组件书写的使用文档,在做为子组件构建新组件(页面)时,子组件API要求使用者输入组件类型,让使用者知道该定义哪些字段(类型提示)并确保字段值类型正确(类型约束和检测)。这样实现了一个页面中所有子组件之间的类型耦合,无论组件嵌套多少层,无论哪层组件数据类型发生改变,所有相关组件类型都会得到感知。当您修改、重构代码时,只要无类型报错(tsc --noEmit --watch)就不会有运行时报错的心智负担。 + 新的组件构建API(DefineComponent)返回的类型叫组件(文档)类型,好比传统组件(UI)库为每个组件书写的使用文档,在做为子组件构建新组件(页面)时,子组件API(SubComponent)要求使用者输入组件类型,在定义选项字段时,会得到类型提示、约束和检测。这样实现了一个页面中所有子组件之间的类型耦合,无论组件嵌套多少层,无论哪层组件数据类型发生改变,所有相关组件类型都会得到感知。当您增改、重构代码时,只要无类型报错(tsc --noEmit --watch)就不会有运行时报错的心智负担。 -- **类型约束代码规范** +- **类型约束书写规范** - js开发时可以任意`setData`一个未声明变量或通过`this.triggerEvent('name',unknown)`触发组件自定义事件。这是js的特点,但易读性和易维护性差。(ts开发时)新API通过类型报错形式对[代码规范](./doc/standard.md)给了强制约束 + annil通过类型来约束代码书写规范,例如在js开发时可以通过`this.setData({...})`新增一个未在data字段中定义的数据或修改properties中定义的数据,这显然是不对的。新API通过类型报错的形式约束[书写规范](./doc/designIdea.md),在特定情形下(例如测试)你可以通过 `//@ts-ignore`或`as any`关闭类型检测。 - **高兼容性** - annil提供的API都是原生API的语法糖,不具有强制性和侵入性,开发者可在整个项目中使用,也可只在某处使用。 + annil提供的API都是原生API的语法糖,不具有强制性和侵入性,可渐进性使用或重构代码。 - **类型修补** - 官方类型(miniprogram-api-typings)存在更新不及时等问题,annil提供了新的类型方便ts开发,同时会对官方类型库发起PR。 - 这些类型都采用ES模块化,不会污染全局类型。 + 官方类型(miniprogram-api-typings)存在更新不及时等问题,annil提供了新的类型方便开发,这些类型都采用ES模块化,不会污染全局类型。 -- **适配任何第三方库** +- **适配第三方库类型** - 通过插件提供的泛型[GenerateDoc](./src/types/GenerateDoc.ts)即可根据组件文档生成组件类型,插件也内置了原生和第三方组件库类型。 + 第三方组件库一般都是以文档的形式对组件说明,插件提供了泛型[GenerateDoc](./src/types/GenerateDoc.ts),可根据组件文档快速书写组件类型,使第三方组件融入组件构建模型,后续插件会陆续添加原生(Wm)和常用第三方组件库(Vant)类型。 ### 安装 @@ -69,6 +71,7 @@ annil(安奈儿)是微信小程序原生开发插件。 - 可选 ```bash + # 使用ts开发 npm typescript --save-dev ``` @@ -115,18 +118,18 @@ annil(安奈儿)是微信小程序原生开发插件。 ### 使用文档 -1. 组件构建API +- **组件构建API** - [DefineComponent](./doc/api/DefineComponent.md) + [DefineComponent](./doc/api/DefineComponent.md) - [RootComopnent](./doc/api/RootComopnent.md) + [RootComopnent](./doc/api/RootComopnent.md) - [SubComponent](./doc/api/SubComponent.md) + [SubComponent](./doc/api/SubComponent.md) -### 更新日志 +- **navigateTo** -[CHANGELOG](./CHANGELOG.md) + [navigateTo](./doc/api/navigateTo.md) -### 协议 +### 更新日志 -[MIT](./LICENSE) +[CHANGELOG](./CHANGELOG.md) diff --git a/doc/api/DefineComponent.md b/doc/api/DefineComponent.md index 8c56d04..59f4e0c 100644 --- a/doc/api/DefineComponent.md +++ b/doc/api/DefineComponent.md @@ -1,6 +1,6 @@ -### DefineComponent 接口 +### DefineComponent -DefineComponent 是组件构建API其中之一,搭配 [RootComponent](./RootComponent.md) 和 [SubComponent](./SubComponent.md)使用 +> DefineComponent 是组件构建函数之一,搭配 [RootComponent](./RootComponent.md) 和 [SubComponent](./SubComponent.md)使用 示例 A (DefineComponent 建立页面时) @@ -19,7 +19,7 @@ const rootComponent = RootComponent()({ }); DefineComponent({ - path: "/pages/index/index", + path: "/pages/index/index", // 构建页面时为 `path` 字段 js可忽略 rootComponent, subComponents: [SubComponentA, SubComponentB], }); @@ -37,24 +37,24 @@ const SubComponentB = SubComponent()({ // ... }); const rootComponent = RootComponent()({ - // 无isPage字段 或 isPage:false + // 不写isPage字段 或 isPage:false }); DefineComponent({ - name: "demo", + name: "compA", // 构建组件时为 `name`字段 js可忽略 rootComponent, subComponents: [SubComponentA, SubComponentB], }); ``` -RootComponent有3个字段 +**字段说明** -1. rootComponent字段即 RootComponent接口返回的对象([RootComponentDoc](../../src\api\RootComponent\RootComponentDoc.ts)类型) +1. rootComponent字段类型为([RootComponentDoc](../../src\api\RootComponent\RootComponentDoc.ts)),即RootComponent接口返回类型 -2. subComponents字段为数组,item是SubComponent接口返回的对象([SubComponentDoc](../../src\api\SubComponent\SubComponentDoc.ts)类型) +2. subComponents字段类型为([SubComponentDoc](../../src\api\SubComponent\SubComponentDoc.ts)[ ]),即SubComponent接口返回的类型数组 -3. path或name字段 +3. path或name字段(js开发可忽略) 当rootComponent字段的isPage为true时(页面),字段为path,类型为`/${string}` 当rootComponent字段的isPage不存在或为false时(组件),字段为name,类型为非空字符串 - ts开发时有类型提示(检测),js开发时有运行时检测。 + ts开发时有类型提示(检测). diff --git a/doc/api/RootComponent.md b/doc/api/RootComponent.md index 2ec33ee..8a7ec29 100644 --- a/doc/api/RootComponent.md +++ b/doc/api/RootComponent.md @@ -1,34 +1,70 @@ -### RootComponent 接口 +### RootComponent -DefineComponent 是组件构建API其中之一,搭配 [RootComponent](./RootComponent.md) 和 [SubComponent](./SubComponent.md)使用,其选项继承大多原生Component接口选项(暂不支持 "definitionFilter","export"),新增和修改选项如下: +RootComponent 是组件构建函数之一,搭配 [DefineComponent](./DefineComponent.md) 和 [SubComponent](./SubComponent.md)使用,支持所有原生[Component] API选项,并增加了新的功能选项,具体如下: -**新增** +> **特别注意:为了使用外部泛型,接口采用高阶函数,需要2次调用** -- isPage +示例: - 是否可选: 页面时必需,组件时可选 +```ts +RootComponent()({ + properties: { + // ... + }, + data: { + // ... + }, + // ... +}); +``` + +- **isPage** + + 是否可选: 页面时必需为true,组件时可选 类型:boolean 默认: false - 解释: 表示创建的组件类型(true为页面,false为组件),影响pageLifetime字段类型和DefineComponent接口的字段(path/name) + 说明: 表示创建的实例类型(true为页面,false为组件),影响pageLifetime字段类型和DefineComponent选项字段(path/name),有运行时检测。 [示例A和B](./DefineComponent.md) -- computed +- **properties** 是否可选: 可选 - 类型: boolean + 类型:[PropertiesConstraint](../../src/api/RootComponent/Properties/PropertiesConstraint.ts) - 默认: {} + 默认: { } + + 说明: js开发时同原生Component的properties选项。ts开发时,字段类型不允许null,对象配置去除observer字段,可通过as [DetailedType](../../src/types/DetailedType.ts)定义任意类型,有字段检测和value类型检测。 + + 示例: + + [定义必需字段示例](../../src/api/RootComponent/Properties/test/normalRequired.test.ts) + + [定义可选字段示例](../../src/api/RootComponent/Properties/test/normalOptional.test.ts) + + [类型错误示例](../../src/api/RootComponent/Properties/test/error.test.ts) - 解释: 定义实例的计算属性,函数体内可通过this.data获取实例数据,依赖实例的数据变化时会自动更新实例上定义字段的值。 +- **computed** + + 是否可选: 可选 + + 类型: [ComputedConstraint](../../src/api/RootComponent/Computed/ComputedConstraint.ts) + + 默认: { } + + 说明: 定义实例的计算属性,函数体内可通过this.datay引用其他实例数据(包括其他计算属性),依赖的实例数据变化时会自动更新。 + + [标准示例](../../src/api/RootComponent/Computed/test/normal.test.ts) + + [类型错误示例](../../src/api/RootComponent/Computed/test/error.test.ts) [测试用例](../../jest/computed/normal/normal.ts) -- store +- **store** 是否可选: 可选 @@ -36,12 +72,16 @@ DefineComponent 是组件构建API其中之一,搭配 [RootComponent](./RootComp 默认: {} - 解释: 定义实例上的响应式数据字段(基于mobx),当store数据发生变化,实例数据跟随变化。 - 可通过实例方法disposer取消对store变化的监控(this.disposer.xxx()) + 说明: 定义引入的全局响应式数据字段(基于mobx),当store数据发生变化,实例数据随之变化。 + 可通过disposer取消对定义字段的监控(this.disposer.storeFields()) + + [标准示例](../../src/api/RootComponent/Store/test/normal.test.ts) + + [类型错误示例](../../src/api/RootComponent/Store/test/error.test.ts) [测试用例](../../jest/store/store.ts) -- events +- **events** 是否可选: 可选 @@ -49,11 +89,15 @@ DefineComponent 是组件构建API其中之一,搭配 [RootComponent](./RootComp 默认: {} - 解释: 定义组件事件,参数e默认为基础事件类型,可(使用Detail,Dataset,Mark等泛型)自定义事件参数类型。 + 说明: 定义组件事件,参数e默认为基础事件类型,可(使用Detail,Dataset,Mark等泛型)自定义事件参数类型。当有子组件泛型传入时(ts),后冒泡/捕获/阻止事件提示字段 + + [标准示例](../../src/api/RootComponent/Events/test/normal.test.ts) + + [类型错误示例](../../src/api/RootComponent/Events/test/error.test.ts) [测试用例](../../jest/events/events.ts) -- customEvents +- **customEvents** 是否可选: 可选 @@ -61,11 +105,15 @@ DefineComponent 是组件构建API其中之一,搭配 [RootComponent](./RootComp 默认: {} - 解释: 定义组件自定义事件,通过this直接触发 + 说明: 定义组件自定义事件,通过this直接触发。在构建页面(isPage:true)时,无此字段。 + + [标准示例](../../src/api/RootComponent/CustomEvents/test/normal.test.ts) + + [类型错误示例](../../src/api/RootComponent/CustomEvents/test/error.test.ts) [测试用例](../../jest/customEvents/customEvents.ts) -- watch +- **watch** 是否可选: 可选 @@ -73,6 +121,84 @@ DefineComponent 是组件构建API其中之一,搭配 [RootComponent](./RootComp 默认: {} - 解释: 功能同原生observers字段,与observers不同的是深度比较 + 说明: 功能同原生observers字段,与observers不同的是深度相等比较和参数有旧值 + + [监控properties](../../src/api/RootComponent/Watch/test/WatchProperties.test.ts) + + [监控computed](../../src/api/RootComponent/Watch/test/WatchComputed.test.ts) + + [监控data](../../src/api/RootComponent/Watch/test/WatchData.test.ts) + + [监控注入的store](../../src/api/RootComponent/Watch/test/WtachInject.test.ts) + + [类型错误示例](../../src/api/RootComponent/Watch/test/error.test.ts) + + 测试用例目录 `../../jest/watch` + +- **pageLifetimes** + + 是否可选: 可选 + + 类型: [PageLifetimesOption](../../src/api/RootComponent/PageLifetimes/PageLifetimesOption.ts) + + 默认: {} + + 说明: 构建组件(isPage不为true)时选项为[组件所在页面的生命周期](https://developers.weixin.qq.com/miniprogram/dev/framework/custom-component/lifetimes.html#%E7%BB%84%E4%BB%B6%E6%89%80%E5%9C%A8%E9%A1%B5%E9%9D%A2%E7%9A%84%E7%94%9F%E5%91%BD%E5%91%A8%E6%9C%9F),并加入了load生命周期(要求最低基础库 3.0.0)。构建页面时字段为[写在methods中的页面生命周期](https://developers.weixin.qq.com/miniprogram/dev/framework/custom-component/component.html)。 + + [标准示例](../../src/api/RootComponent/PageLifetimes/test/normal.test.ts) + + [错误示例](../../src/api/RootComponent/PageLifetimes/test/error.test.ts) + +- **lifetimes** + + 同原生[Component] API 的lifetimes选项(新增beforeCreate生命周期) + + [标准示例](../../src/api/RootComponent/Lifetimes/test/normal.test.ts) + + [错误示例](../../src/api/RootComponent/Lifetimes/test/error.test.ts) + +- **observers** + + 同原生[Component] API 的observers选项 + + [标准示例](../../src/api/RootComponent/Observers/test/normal.test.ts) + +- **data** + + 同原生[Component] API 的data选项 + + [标准示例](../../src/api/RootComponent/Data/test/normal.test.ts) + + [错误示例](../../src/api/RootComponent/Data/test/error.test.ts) + +- **methods** + + 同原生[Component] API 的methods选项 + + [标准示例](../../src/api/RootComponent/Methods/test/mormal.test.ts) + + [错误示例](../../src/api/RootComponent/Methods/test/error.test.ts) + +- **export** + + 同原生[Component] API 的export选项 当包含`behaviors:['wx://component-export']` 时有效 + +- **externalClasses** + + 同原生[Component] API 的externalClasses选项。 + +- **options** + + 同原生[Component] API 的options选项。可通过[实例注入]()的方式避免每个组件重复定义相同类型。 + +- **relations** + + 同原生[Component] API 的relations选项。 + +- **behaviors** + + 同原生[Component] API 的behaviors选项。 + +> 源码路径`/src/api/RootComponent/` - [测试用例](../../jest/watch/watch.ts) +[Component]: https://developers.weixin.qq.com/miniprogram/dev/reference/api/Component.html diff --git a/doc/api/SubComponent.md b/doc/api/SubComponent.md index e69de29..452b2a3 100644 --- a/doc/api/SubComponent.md +++ b/doc/api/SubComponent.md @@ -0,0 +1,129 @@ +### SubComponent + +SubComponent 是组件构建函数之一,搭配 [DefineComponent](./DefineComponent.md)和[SubComponent]使用。**特别注意:为了使用外部泛型,接口采用高阶函数,需要2次调用** +各选项说明如下: + +- **inherit** + + 是否可选: 可选 + + 类型: [InheritConstraint](../../src/api/SubComponent/SubInherit/SubInheritConstraint.ts) + + 默认: { } + + 说明:js可忽略,作为ts开发时类型辅助字段 + + [标准示例](../../src/api/SubComponent/SubInherit/test/normal.test.ts) + + [类型错误示例](../../src/api/SubComponent/SubInherit/test/error.test.ts) + +- **data** + + 是否可选: 可选 + + 类型: [SubData](../../src/api/SubComponent/SubData/SubDataConstraint.ts) + + 默认: { } + + [标准示例](../../src/api/SubComponent/Subdata/test/normal.test.ts) + + [类型错误示例](../../src/api/SubComponent/Subdata/test/error.test.ts) + +- **computed** + + 是否可选: 可选 + + 类型: [SubComputedConstraint](../../src/api/SubComponent/SubComputed/SubComputedConstraint.ts) + + 默认: { } + + 说明: + + [标准示例](../../src/api/SubComponent/SubComputed/test/normal.test.ts) + + [类型错误示例](../../src/api/SubComponent/SubComputed/test/error.test.ts) + + [测试用例](../../jest/computed/normal/normal.ts) + +- **store** + + 是否可选: 可选 + + 类型: [SubStoreConstraint](../../src/api/SubComponent/SubStore/SubStoreConstraint.ts) + + 默认: {} + + 说明: 定义引入的全局响应式数据字段(基于mobx),当store数据发生变化,实例数据随之变化。 + 可通过disposer取消对定义字段的监控(this.disposer.storeFields()) + + [标准示例](../../src/api/SubComponent/SubStore//test/normal.test.ts) + + [类型错误示例](../../src/api/SubComponent/SubStore/test/error.test.ts) + + [测试用例](../../jest/store/store.ts) + +- **events** + + 是否可选: 可选 + + 类型: [EventsConstraint](../../src/api/SubComponent/SubEvents/SubEventsConstraint.ts) + + 默认: {} + + 说明: 定义组件事件,参数e默认为基础事件类型,可(使用Detail,Dataset,Mark等泛型)自定义事件参数类型。当有子组件泛型传入时(ts),后冒泡/捕获/阻止事件提示字段 + + [标准示例](../../src/api/SubComponent/SubEvents/test/normal.test.ts) + + [类型错误示例](../../src/api/SubComponent/SubEvents/test/error.test.ts) + + [测试用例](../../jest/events/events.ts) + +- **watch** + + 是否可选: 可选 + + 类型: [SubWatchOption](../../src/api/SubComponent/SubWatch/SubWatchOption.ts) + + 默认: {} + + 说明: 功能同原生observers字段,与observers不同的是深度相等比较和参数有旧值 + + [监控properties](../../src/api/SubComponent/SubWatch/test/WatchProperties.test.ts) + + [监控computed](../../src/api/SubComponent/SubWatch/test/WatchComputed.test.ts) + + [监控data](../../src/api/SubComponent/SubWatch/test/WatchRootData.test.ts) + + [监控注入的store](../../src/api/SubComponent/SubWatch/test/WtachInject.test.ts) + + [类型错误示例](../../src/api/SubComponent/SubWatch/test/error.test.ts) + + [测试用例](../../jest/watch/) + +- **pageLifetimes** + + 同[RootComponent]的PageLifetimes + +- **lifetimes** + + 同[RootComponent]的lifetimes + +- **observers** + + 同[RootComponent]的observers + +- **methods** + + 同[RootComponent]的methods,有字段前缀检测 + +- **behaviors** + + 同原生[Component] API 的behaviors选项。 + +- **externalClasses** + + 同原生[Component] API 的externalClasses选项。 + +> 源码路径`/src/api/SubComponent/` + +[RootComponent]: ./RootComponent.md diff --git a/doc/api/navigateTo.md b/doc/api/navigateTo.md new file mode 100644 index 0000000..742a4e4 --- /dev/null +++ b/doc/api/navigateTo.md @@ -0,0 +1,66 @@ +### navigateTo + +> 原生wx.navigateTo的语法糖,搭配组件构建API使用,在onLoad(load)生命周期中可直接获取值并支持特殊字符`:/?#[]@!$&'()*+,;=` + +示例 + +```ts +// pages/demo/demo.ts + +export type User = { + name: string; + age?: number; +}; +const rootComponent = RootComonent()({ + properties: { + user: Object as DetailedType, + character: String, + }, + pageLifetimes: { + onLoad(params) { + console.log(params.user); // { name:"annil",age: 23 } + console.log(params.character); // ":/?#[]@!$&'()*+,;=" + }, + }, +}); + +const demo = DefineComonent({ + path: "pages/demo/demo", + rootComponent, +}); +export type $Demo = typeof demo; + +// type $Demo = { +// path: "pages/demo/demo"; +// properties: { +// user: User; +// character:string +// }; +// }; +``` + +```ts +// pages/index/index.ts +import { navigateTo } from "annil"; +import type { $Demo, User } from "path/to/demo"; +Page({ + data: { + user: { + name: "annil", + age: 23, + }, + }, + onLoad() { + navigateTo<$Demo>({ + url: "pages/demo/demo", + data: { + user: this.data.user, + character: ":/?#[]@!$&'()*+,;=", + }, + // ... + }); + }, +}); +``` + +> 从基础库3.0.0起, 组件的pageLifetimes选项中加入了load生命周期,当使用navigateTo传值时,load参数得到的值与页面onLoad周期得到的参数相同。 diff --git a/doc/designIdea.md b/doc/designIdea.md new file mode 100644 index 0000000..8dac040 --- /dev/null +++ b/doc/designIdea.md @@ -0,0 +1,324 @@ +### 组件构建(设计)方案 + +> annil在构建组件(页面)时采用根组件(RootComponent)与子组件(SubComponent)组合的方式。需要注意的是,为了接收外部泛型,RootComponent和SubComponent都是高阶函数,需要两次调用。 + +1. 基础组件(baseComp) + + ```ts + // components/baseComp/baseComp.ts + import { DefineComponent, type DetailedType, RootComponent } from "annil"; + export type Gender = "male" | "female"; + export type Age = 16 | 17 | 18; + const rootComponent = RootComponent()({ // 切记 两次调用 + properties: { + gender: Object as DetailedType, // 必传属性 + age: { // 选传属性 + type: Number as DetailedType, + value: 16, // 有字段检测和类型检测 + }, + }, + customEvents: { + tap: String as DetailedType, + }, + events: { + onTap() { + const { gender } = this.data; + + this.tap(gender); // this.triggerEvent('tap',gender) + }, + }, + // ... + }); + const baseComp = DefineComponent({ + name: "baseComp", + rootComponent, + // subComponents:[] + }); + export type $BaseComp = typeof baseComp; + + // 组件文档类型 + // type $BaseComp = { + // properties: { + // baseComp_gender: Gender; // 必传属性 + // baseComp_age?: number; // 选传属性 + // }; + // customEvents: { // 组件事件 + // baseComp_tap: Gender; + // }; + // } + ``` + +2. 页面(index) + + ```html + + + + + ``` + + ```ts + // index.ts + import { DefineComponent, type DetailedType, RootComponent } from "annil"; + import type { $BaseComp, Age, Gender } from "path/to/baseComp"; + export type User = { + name: string; + age?: number; + }; + // 建立一个子组件(baseComp)选项配置 + const baseComp = SubComponent()({ // 切记 两次调用 + inherit: { // 继承字段 用于表达组件需要的数据源自rootComponent的哪个数据。此字段运行时无意义,书写是为了类型验证。 + baseComp_age: "age", + }, + data: { + baseComp_gender: "female", + }, + events: { + baseComp_tap(e) { + const gender = e.detail; + + this.rootMethods(gender); + }, + }, + }); + + type Root = typeof rootComponent; + + const rootComponent = RootComponent()({ // 切记 两次调用 + isPage: true, // 构建的是页面 + properties: { + user: Object as DetailedType, + }, + data: { + age: 16 as Age, + }, + methods: { + rootMethods(gender: Gender) { + console.log(gender); + // do something + }, + }, + // ... + }); + const index = DefineComponent({ + path: "/pages/index/index", + rootComponent, + subComponents: [baseComp], + }); + + export type $Index = typeof index; + // 页面文档类型 + // type $Index = { + // path: "/pages/index/index"; + // properties: { + // user: User; + // }; + // } + ``` + + > 从上面示例可以看出,annil是为ts而生(设计),每个组件(页面)都有自己的类型(好比组件文档,所以也叫组件文档类型)。在复杂组件(页面)中构建子组件代码逻辑时,SubComonent函数需要您输入根组件的类型(泛型参数一)和要构建的组件类型(泛型参数二),这样SubComonent函数会在您配置选项字段时给您字段提示和类型检测,在解决命名冲突和依赖混乱问题的同时,也解耦了组件逻辑代码,从而提高代码可读性和易拓展性。理解了设计思想,annil同样适用js开发,但缺失了ts具有的类型提示和类型检测功能。 + +3. **组件复用同一组件** + + > 有时可能会出现一个复杂组件中多次使用同一组件的情况,此时可通过输入后缀字段(SubComponent的第三个泛型参数)来避免字段重复问题。 + + 示例 demo + + ```html + + + + + + + + ``` + + ```ts + import { DefineComponent, type DetailedType, RootComponent } from "annil"; + import type { $BaseComp, Gender } from "path/to/baseComp"; + + // SubComponent的第三个泛型输入为字段后缀,会以大驼峰形式加在组件前缀后面 + const baseCompA = SubComponent()({ + data: { + baseCompA_gender: "female", + }, + events: { + baseCompA_tap(e) { + const gender = e.detail; + + this.rootMethods(gender); + }, + }, + }); + const baseCompInB = SubComponent()({ + data: { + baseCompInB_gender: "male", + }, + events: { + baseCompInB_tap(e) { + const gender = e.detail; + + this.rootMethods(gender); + }, + }, + }); + type Root = typeof rootComponent; + + const rootComponent = RootComponent()({ + methods: { + rootMethods(gender: Gender) { + console.log(gender); + // do something + }, + }, + // ... + }); + const index = DefineComponent({ + path: "/pages/index/index", + rootComponent, + subComponents: [baseCompA, baseCompInB], + }); + ``` + +### 一些设计思想 + +1. **events** + > 小程序原生API(Component)把事件函数定义在methods字段中固然简洁,但对阅读和维护代码是不友好的,故在RootComponent和SubComopnent接口中加入了events选项,定义在events中的事件函数无法通过this调用(ts类型约束),参数e拥有默认类型,也可自定类型(需在tsconfig中配置`"strictFunctionTypes": false`). + + ```ts + const rootComponent = RootComponent()({ + data: { + num: 1, + }, + events: { + // 事件默认有自己的事件类型e + onTap(e) { + const { num } = this.data; + // ok 事件函数中可以调用实例上的方法 + this.add(num); + }, + }, + methods: { + add(num: number) { + return num + 1; + }, + error(num: number) { + // @ts-expect-error 实例中无事件字段 + this.onTap; + }, + }, + }); + ``` + +2. **customEvents** + + > 传统调用组件自定义事件的写法`this.triggerEvent("tap", { num: 1 }, { bubbles: true,});`是不友好的,RootComponent API提供了customEvents选项。 + + ```ts + type Gender = "male" | "female"; + const rootComponent = RootComponent()({ + data: { + gender: "male" as Gender, + }, + // 自定义事件字段更便于代码阅读和类型友好 + customEvents: { + tap: { + detail: String as DetailedType, + options: { + bubbles: true, + composed: true, + capturePhase: true, + }, + }, + }, + events: { + onTap() { + const { gender } = this.data; + // 触发自定义事件 好比 `this.triggerEvent("tap", "male", { bubbles: true, composed: true, capturePhase: true });` + this.tap(gender); + }, + }, + }); + ``` + +3. **不允许对非data定义字段做数据改变** + + ```ts + // storeUser.ts + import { observable } from "mobx"; + + type User = { name: string; age: number }; + + export const storeUser = observable({ + name: "zhao", + age: 20, + }); + ``` + ```ts + import { DefineComponent, type DetailedType, RootComponent } from "annil"; + import { storeUser, type User } from "storeUser"; + + const rootComponent = RootComponent()({ + properties: { + user: Object as DetailedType, + }, + data: { + num: 1, + }, + store: { + Sage: () => storeUser.age, + }, + computed: { + age() { + return this.data.user?.age || 20; + }, + }, + events: { + onTap() { + this.setData({ + num: 2, // ok + // @ts-expect-error 组件无权对properties字段修改 + user: { name: "zhao", age: 20 }, + // @ts-expect-error 计算字段的更新源自依赖的数据改变,不可手动修改 + age: 30, + // @ts-expect-error 响应式的数据改变源自store自身方法,不可手动修改 + Sage: 40, + }); + }, + }, + }); + ``` + +4. **SubComponent——inherit** + +为了检查子组件配置是否符合传入的子组件类型需求(必传字段检测),需要知道当前子组件选项配置中都定义和使用了哪些数据,若子组件数据使用的是根组件数据,为了告诉类型系统,所以需要一个描述字段——inherit。 +当所有数据字段(inhrit,data,computed,store)不满足组件文档的必传数据时,SubComponent返回的类型是缺少的数据字段(字符串),这不符合DefineComopnent的subComponents字段的类型,从而报错。 + +```ts +import { DefineComponent, type DetailedType, RootComponent } from "annil"; +import type { $BaseComp, Age, Gender } from "path/to/baseComp"; +const baseComp = SubComponent()({ + // inhrit: { + // baseComp_gender: "gender", + // }, + data: { + age: 18 as Age, + }, +}); +type Root = typeof rootComponent; +const rootComponent = RootComponent()({ + data: { + gender: "male" as Gender, + }, + // ... +}); +const index = DefineComponent({ + path: "/pages/index/index", + rootComponent, + // @ts-expect-error 不能将 `缺少组件必传字段 baseComp_gender`类型分配给 _SubComponentDoc + subComponents: [baseComp], +}); +``` + +> 由于 baseComp子组件配置中缺少对$BaseComp类型中必传字段`baseComp_gender`的定义,所以baseComp的类型为`缺少组件必传字段 baseComp_gender`,导致DefineComponentAPI的subComponents字段报错。 diff --git a/doc/fields/computed.md b/doc/fields/computed.md deleted file mode 100644 index 784e422..0000000 --- a/doc/fields/computed.md +++ /dev/null @@ -1,93 +0,0 @@ -### computed字段说明文档 - -1. **RootComponent下的computed字段** - - 可通过this.data引用properties,data,store定义的数据及其他计算属性字段,返回新的字段数据。 - - > 提示:this下仅有data字段(关闭了其他字段),且this.data内部数据为只读,设计为this.data形式是便于ts类型推导也符合小程序调用数据习惯。 - - ```ts - import { DefineComponent, type DetailedType, RootComponent } from "annil"; - import { observable } from "mobx"; - - type User = { name: string; age: number }; - - const storeUser = observable({ - name: "zhao", - age: 20, - }); - - const rootComponent = RootComponent()({ - properties: { - user: Object as DetailedType, - userOptional: { - type: Object as DetailedType, - value: { name: "zhao", age: 20 }, - }, - }, - data: { - Duser: { name: "zhao", age: 20 }, - }, - store: { - Sage: () => storeUser.age, - }, - - computed: { - // 1. 引用必传的properties字段 - age() { - return this.data.user?.age || 0; - }, - // 2. 引用可选的properties字段 - name() { - return this.data.userOptional?.name || "zhao"; - }, - // 3. 引用data字段 - Dname() { - return this.data.Duser.name; - }, - // 4. 引用store字段 - SagePlus() { - return this.data.Sage + 1; - }, - // 5. 引用其他计算属性 - refOtherComputedFields() { - return this.data.age + this.data.SagePlus; - }, - // 6. 运行时报错 - isError() { - // @ts-expect-error 'num字段是只读的' - this.data.num = 1; - - return 123; - }, - // 7. ts下重复字段报错(与properties,data,store下字段对比) - // @ts-expect-error 与data字段重复 - Duser() { - return "xxx"; - }, - }, - }); - - const computed = DefineComponent({ - name: "computed", - rootComponent, - }); - - export type $Computed = typeof computed; - ``` - - [类型标准示例](../../src/api/RootComponent/Computed/test/normal.test.ts) - - [类型错误示例](../../src/api/RootComponent/Computed/test/error.test.ts) - - [单元测试示例](../../jest/computed/normal/normal.ts) - -2. **SubComponent下的computed字段** - - 使用同1,但在ts下有严格的类型约束(不允许定义非组件所需数据字段) - - [类型标准示例](../../src/api/SubComponent/SubComputed/test/normal.test.ts) - - [类型错误示例](../../src/api/SubComponent/SubComputed/test/error.test.ts) - - [单元测试示例](../../jest/computed/normal/normal.ts) diff --git a/doc/fields/store.md b/doc/fields/store.md deleted file mode 100644 index b00fb53..0000000 --- a/doc/fields/store.md +++ /dev/null @@ -1,59 +0,0 @@ -### store字段说明文档 - -可引入基于mobx的全局状态(响应式数据)。 - -```ts -import { DefineComponent, type DetailedType, RootComponent } from "annil"; -import { observable } from "mobx"; - -type User = { name: string; age: number }; - -const storeUser = observable({ - name: "zhao", - age: 20, - changeName(name: string) { - this.name = name; - }, - changeAge(age: number) { - this.age = age; - }, -}); -type DemoComp = { properties: { demo_age: number } }; -const rootComponent = SubComopnent<{}>()({ - store: { - demo_age: () => storeUser.age, - }, - lifetimes: { - attached() { - console.log(this.data.demo_age); // 20 - storeUser.changeAge(30); - console.log(this.data.demo_age); // 30 - }, - }, -}); -const rootComponent = RootComponent()({ - store: { - name: () => storeUser.name, - }, - lifetimes: { - attached() { - console.log(this.data.name); // 'zhao' - storeUser.changeName("li"); - console.log(this.data.name); // 'li' - }, - }, -}); - -const store = DefineComponent({ - name: "store", - rootComponent, -}); - -export type $Store = typeof store; -``` - -[类型标准示例](../../src/api/RootComponent/store/test/normal.test.ts) - -[类型错误示例](../../src/api/RootComponent/store/test/error.test.ts) - -[单元测试示例](../../jest/store/store.ts) diff --git a/doc/fields/watch.md b/doc/fields/watch.md deleted file mode 100644 index 2bcc91d..0000000 --- a/doc/fields/watch.md +++ /dev/null @@ -1,99 +0,0 @@ -### watch字段说明文档 - -1. RootComponent.watch - - 可定义(监控)所有数据字段(properties,data,store,computed中定义的),数据改变时(深度相等比较)运行定义函数,参数包含旧值,可监控多个字段或子字段。 - - > 提示:ts开发时,监控computed字段需要手动写入类型(鼠标悬停在定义字段上可看到类型),单一子字段给了类型提示,多字段没有类型提示要手动ignore类型错误。 - - ```ts - import { DefineComponent, type DetailedType, RootComponent } from "annil"; - import { observable } from "mobx"; - - type User = { name: string; age: number }; - - const storeUser = observable({ - name: "zhao", - age: 20, - changeName(name: string) { - this.name = name; - }, - }); - - const rootComponent = RootComponent()({ - properties: { - user: Object as DetailedType, // 默认null - }, - data: { - Duser: { name: "zhao", age: 20 }, - }, - store: { - name: () => storeUser.name, - }, - computed: { - // 1. 引用必传的properties字段 - age() { - return this.data.user?.age || 0; - }, - }, - watch: { - user(newValue, oldValue) { - console.log(newValue, oldValue); // {name:'li',age:30}, null - }, - "Duser.age"(newValue, oldValue) { - console.log(newValue, oldValue); // 30, 20 - }, - name(newValue, oldValue) { - console.log(newValue, oldValue); // "li", "zhao" - }, - age(newValue, oldValue) { - console.log(newValue, oldValue); // 30, 0 - }, - }, - lifetimes: { - attached() { - this.setData({ - "Duser.age": 30, - // 模拟properteis.user传入新值 - user: { name: "li", age: 30 }, - }); - storeUser.changeName("li"); - }, - }, - }); - - const watch = DefineComponent({ - name: "watch", - rootComponent, - }); - - export type $Watch = typeof computed; - ``` - - [类型测试watchProperties](../../src/api/RootComponent/Watch/test/WatchProperties.test.ts) - - [类型测试watchData](../../src/api/RootComponent/Watch/test/WatchData.test.ts) - - [类型测试watchComputed](../../src/api/RootComponent/Watch/test/WatchComputed.test.ts) - - [类型错误示例](../../src/api/RootComponent/Watch/test/error.test.ts) - - [单元测试示例watch-computed](../../jest/watch/computed/computed.ts) - - [单元测试示例watch-data](../../jest/watch/data/data.ts) - - [单元测试示例watch-multipleFields](../../jest/watch/multipleFields/multipleFields.ts) - - [单元测试示例watch-properties](../../jest/watch/properties/properties.ts) - -2. **SubComponent下的watch字段** - - 不仅可定义根组件数据还可以定义自身定义数据 - - [类型测试watchSubData](../../src/api/SubComponent/SubWatch/test/WatchSubData.test.ts) - - [类型测试watchRootData](../../src/api/SubComponent/SubWatch/test/WatchRootData.test.ts) - - [类型测试watchSubComputed](../../src/api/SubComponent/SubWatch/test/WatchComputed.test.ts) - - [类型错误示例](../../src/api/SubComponent/SubWatch/test/error.test.ts) diff --git a/doc/standard.md b/doc/standard.md deleted file mode 100644 index c3dccf4..0000000 --- a/doc/standard.md +++ /dev/null @@ -1,106 +0,0 @@ -### 书写规范 - -> 书写规范是通过ts类型约束的,包括但不限于以下情况。 - -1. 方法和事件应该分别定义 - > 把事件定义在方法字段中固然简洁,但对阅读和维护代码是不友好的,很容易写出手动调用事件方法的错误代码。 - - ```ts - const rootComponent = RootComponent()({ - data: { - num: 1, - }, - events: { - // 事件默认有自己的事件类型e - onTap(e) { - const { num } = this.data; - // ok 事件函数中可以调用实例上的方法 - this.add(num); - }, - }, - // 方法中无法调用事件方法 - methods: { - add(num: number) { - return num + 1; - }, - error(num: number) { - // @ts-expect-error 实例中无事件字段 - this.onTap; - }, - }, - }); - ``` - -1. 自定义事件应提前定义 - - > 传统写法`this.triggerEvent("tap", { num: 1 }, { bubbles: true,});`是不友好的 - - ```ts - const rootComponent = RootComponent()({ - data: { - num: 1, - }, - // 自定义事件字段更便于代码阅读和类型友好 - customEvents: { - tap: { - detail: Number, - options: { - bubbles: true, - composed: true, - capturePhase: true, - }, - }, - }, - events: { - onTap() { - const { num } = this.data; - // 触发自定义事件 - this.tap(num); - }, - }, - }); - ``` - -1. 不允许对非data定义字段做数据改变 - - ```ts - import { DefineComponent, type DetailedType, RootComponent } from "annil"; - import { observable } from "mobx"; - - type User = { name: string; age: number }; - - const storeUser = observable({ - name: "zhao", - age: 20, - }); - - const rootComponent = RootComponent()({ - properties: { - user: Object as DetailedType, - }, - data: { - num: 1, - }, - store: { - Sage: () => storeUser.age, - }, - computed: { - age() { - return this.data.user?.age || 0; - }, - }, - events: { - onTap() { - this.setData({ - num: 2, // ok - // @ts-expect-error 组件无权对properties字段修改 - user: { name: "zhao", age: 20 }, - // @ts-expect-error 计算字段的更新源自依赖的数据改变,不可手动修改 - age: 30, - // @ts-expect-error 响应式的数据改变源自store自身方法,不可手动修改 - Sage: 40, - }); - }, - }, - }); - ``` diff --git a/jest.config.ts b/jest.config.ts index 1564478..f748fbd 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -12,6 +12,7 @@ export const config = { "./src/**/*.ts", "!src/**/*.test.ts", "!src/api/navigateTo.ts", + "!src/api/DefineComponent/isPageCheck.ts", "!src/api/InstanceInject/inject.ts", "!src/thirdLib/**", ], @@ -24,10 +25,10 @@ export const config = { coverageReporters: ["text", "text-summary"], coverageThreshold: { global: { - branches: 96, - functions: 95, - lines: 95, - statements: 95, + branches: 90, + functions: 90, + lines: 90, + statements: 90, }, }, // coverageDirectory: "coverage", diff --git a/jest/computed/normal/normal.ts b/jest/computed/normal/normal.ts index b608bc4..5571ac1 100644 --- a/jest/computed/normal/normal.ts +++ b/jest/computed/normal/normal.ts @@ -26,6 +26,10 @@ const subA = SubComponent()({ type Root = typeof rootComponent; const rootComponent = RootComponent()({ + // 为了覆盖率 忽略它 + export() { + return {}; + }, properties: { requiredUser: Object as DetailedType, optionalUser: { diff --git a/src/api/DefineComponent/collectOptionsForComponent.ts b/src/api/DefineComponent/collectOptionsForComponent.ts index e9f326a..caf2f39 100644 --- a/src/api/DefineComponent/collectOptionsForComponent.ts +++ b/src/api/DefineComponent/collectOptionsForComponent.ts @@ -52,6 +52,28 @@ export type FinalOptionsOfComponent = { pageLifetimes?: PageLifetimesOption["pageLifetimes"]; }; +/** + * 劫持attached,根据this.route做判断 isPage值是否正确 + */ +/* istanbul ignore next: miniprogram-simulate(当前版本 1.6.1) 无法测试页面 */ +export function isPageCheck(isPage: boolean | undefined) { + return function(this: Instance) { + const route = (this as PageInstance).route as string | undefined; + if (route && isPage !== true) { + // 页面isPage值错误 + throw Error( + `页面 /${route} 中, RootComponent构建页面时,isPage字段值应为 true`, + ); + } + if (!route && isPage !== false && isPage !== undefined) { + // 组件写了isPage = true + throw Error( + `组件 ${this.is} 中 RootComponent构建组件时,可不写isPage字段或值为 false`, + ); + } + }; +} + /** * 原生Component会对传入的对象字段匹配的properties字段setData赋值。不符合字段或Page时不会赋值。 * 此函数为给实例setData赋值,默认传递值与properties相符(ts类型安全)。 @@ -80,27 +102,7 @@ function onLoadReceivedDataHandle( this.setData(option); } -// 类型保护 -// function isPageInstance(ins: Instance): ins is PageInstance & InstanceCustomFields { -// return (ins as PageInstance).route !== undefined; -// } -/** - * 验证path是否正确 - * @param path - 配置路径 - * @returns - */ -// function pathCheck(path: string | undefined) { -// return function(this: Instance) { -// if (isPageInstance(this)) { -// const route = this.route; -// if (route !== path) { -// throw Error( -// `[ /${route} ] DefinedComponent的配置字段path值错误,应为: /${route}`, -// ); -// } -// } -// }; -// } + /** * 针对通过 navigateTo传过来的数据对组件load周期传入数据解析 * @param option - option中的url是拼接了encodeURIComponent转码的data对象的,key为INNERMARKER.url @@ -218,12 +220,17 @@ function otherFieldsHandle( for (const key in rootComponentOptions) { // @ts-ignore 隐式索引 const config = rootComponentOptions[key]; - if (key === "behaviors" || key === "externalClasses") { - // 是不是只有behaviors是数组 + if (Array.isArray(config) === true) { + // "behaviors" || "externalClasses"是数组 + // @ts-ignore 只有 behaviors 和 externalClasses, 且都默认为[] finalOptions[key].push(...config); - } else { + } else if (typeof config === "object") { // @ts-ignore 隐式索引 Object.assign(finalOptions[key] ||= {}, config); + } else { + // 函数字段有 根组件有 `export` 子组件无此字段 + // @ts-ignore 隐式索引 + finalOptions[key] = config; } } } @@ -381,9 +388,19 @@ export function collectOptionsForComponent( && hijack(finalOptionsForComponent.pageLifetimes ||= {}, "load", [loadReceivedDataHandle], []); /* istanbul ignore next: miniprogram-simulate(当前版本 1.6.1) 无法测试页面 */ - hijack(finalOptionsForComponent.methods, "onLoad", [onLoadReceivedDataHandle], []); - - // hijack(finalOptionsForComponent.lifetimes!, "attached", [pathCheck(defineComponentOption.path)], []); + hijack( + finalOptionsForComponent.methods, + "onLoad", + [onLoadReceivedDataHandle], + [], + ); + + hijack( + finalOptionsForComponent.lifetimes!, + "attached", + [isPageCheck(rootComponentOption?.isPage)], + [], + ); /* istanbul ignore next: miniprogram-simulate(当前版本 1.6.1) 无法测试页面 */ finalOptionsForComponent.isPage diff --git a/src/api/RootComponent/Data/test/normal.test.ts b/src/api/RootComponent/Data/test/normal.test.ts index 8beafd8..f60a41f 100644 --- a/src/api/RootComponent/Data/test/normal.test.ts +++ b/src/api/RootComponent/Data/test/normal.test.ts @@ -8,7 +8,7 @@ type Gender = "male" | "female"; const RootDoc = RootComponent()({ data: { - gender: "male", // 联合字面量 + gender: "male" as Gender, // 联合字面量 num: 123, _innernalFields: 123, // 内部字段无法在wxml中使用 }, diff --git a/src/api/RootComponent/Events/test/normal.test.ts b/src/api/RootComponent/Events/test/normal.test.ts index 59d5d33..30afdac 100644 --- a/src/api/RootComponent/Events/test/normal.test.ts +++ b/src/api/RootComponent/Events/test/normal.test.ts @@ -24,7 +24,7 @@ RootComponent()({ }); /** - * 2. 没有子组件时,事件参数e可通过WMBaseEvent和WMCustomEvent(多一个detail类型)定义内部类型。 + * 2. 没有子组件时,事件参数e可通过WMBaseEvent、WMCustomEvent、Detail、Mark等插件提供的类型快速定义事件类型。 */ RootComponent()({ events: { @@ -122,7 +122,7 @@ type RootDocExpect = { }; }; -// 4 返回EventsDoc正常 +// 4 返回EventsDoc Checking; /** diff --git a/src/api/RootComponent/Lifetimes/LifetimesOption.ts b/src/api/RootComponent/Lifetimes/LifetimesOption.ts index af66df6..caaa5b3 100644 --- a/src/api/RootComponent/Lifetimes/LifetimesOption.ts +++ b/src/api/RootComponent/Lifetimes/LifetimesOption.ts @@ -1,34 +1,31 @@ import type { LifetimesConstraint } from "./LifetimesConstraint"; -export type LifetimesOption = TIsPage extends false // 组件开通lifetimes字段 - ? { - /** - * 在原生类型上增加了beforeCreate周期 - * @example - * ```ts - * RootComponent()({ - * lifetimes: { - * beforeCreate(opitons) { - * opitons;//为最终进入Component的配置对象 - * }, - * created() { - * }, - * attached() { - * }, - * ready() { - * }, - * detached() { - * }, - * error(err) { - * err.cause; - * }, - * moved() { - * }, - * }, - * }); - * ``` - */ - lifetimes?: LifetimesConstraint; - } - // 页面关闭lifetimes字段(其实也可以开启) - : unknown; +export type LifetimesOption = { + /** + * 在原生类型上增加了beforeCreate周期 + * @example + * ```ts + * RootComponent()({ + * lifetimes: { + * beforeCreate(opitons) { + * opitons;//为最终进入Component的配置对象 + * }, + * created() { + * }, + * attached() { + * }, + * ready() { + * }, + * detached() { + * }, + * error(err) { + * err.cause; + * }, + * moved() { + * }, + * }, + * }); + * ``` + */ + lifetimes?: LifetimesConstraint; +}; diff --git a/src/api/RootComponent/Lifetimes/test/error.test.ts b/src/api/RootComponent/Lifetimes/test/error.test.ts index f28c133..e35d0dc 100644 --- a/src/api/RootComponent/Lifetimes/test/error.test.ts +++ b/src/api/RootComponent/Lifetimes/test/error.test.ts @@ -11,13 +11,7 @@ RootComponent()({ err.message; }, moved() {}, - // @ts-expect-error 1 错误的声明周期字段报错 + // @ts-expect-error 1 错误的生命周期字段报错 xxx() {}, }, }); - -RootComponent()({ - isPage: true, - // @ts-expect-error 2 页面时未开启此字段 - lifetimes: {}, -}); diff --git a/src/api/RootComponent/Methods/test/mormal.test.ts b/src/api/RootComponent/Methods/test/mormal.test.ts index 30848d5..ae3a7d5 100644 --- a/src/api/RootComponent/Methods/test/mormal.test.ts +++ b/src/api/RootComponent/Methods/test/mormal.test.ts @@ -9,7 +9,7 @@ const RootDoc = RootComponent()({ num() { return 1; }, - str(str: string): string { + str(str: string) { return str; }, }, diff --git a/src/api/RootComponent/PageLifetimes/PageLifetimesOption.ts b/src/api/RootComponent/PageLifetimes/PageLifetimesOption.ts index 786a8ca..0947804 100644 --- a/src/api/RootComponent/PageLifetimes/PageLifetimesOption.ts +++ b/src/api/RootComponent/PageLifetimes/PageLifetimesOption.ts @@ -6,9 +6,6 @@ export type PageLifetimesOption @@ -17,7 +14,7 @@ export type PageLifetimesOption void; + load?: (props?: object) => void; } >; }, diff --git a/src/api/RootComponent/PageLifetimes/test/normal.test.ts b/src/api/RootComponent/PageLifetimes/test/normal.test.ts index af92bf4..ba54075 100644 --- a/src/api/RootComponent/PageLifetimes/test/normal.test.ts +++ b/src/api/RootComponent/PageLifetimes/test/normal.test.ts @@ -7,16 +7,15 @@ import type { Mock_User } from "../../Properties/test/normalRequired.test"; * 组件时 */ RootComponent()({ - // 1 官方类型 加入load类型(同步组件时触发) pageLifetimes: { hide() {}, resize(size) { Checking; }, show() {}, - // 新增周期函数 + // 1 glass-easel(最低版本库3.0.2)支持的周期函数 load(obj) { - Checking; + Checking; }, }, }); @@ -40,7 +39,7 @@ RootComponent()({ }, // 2 官方是把事件写在methods字段中,更改为写在pageLifetimes字段下 pageLifetimes: { - // 3 重写onLoad周期参数props的类型. (propertiesDoc中必传字段去除null后去除可选字段) + // 3 重写onLoad周期参数props的类型(同页面properties定义类型). onLoad(props) { Checking< typeof props, @@ -56,5 +55,15 @@ RootComponent()({ onHide() { console.log("onHide"); }, + // onReady + // onResize + // onUnload + // onPageScroll + // onPullDownRefresh + // onReachBottom + // onShareAppMessage + // onShareTimeline + // onTabItemTap + // ... }, }); diff --git a/src/api/RootComponent/RootComponentDoc.ts b/src/api/RootComponent/RootComponentDoc.ts index 34280ca..fb57417 100644 --- a/src/api/RootComponent/RootComponentDoc.ts +++ b/src/api/RootComponent/RootComponentDoc.ts @@ -1,4 +1,3 @@ -import type { Func } from "hry-types/src/Misc/Func"; import type { WMCompPageLifetimes, WMPageLifetimes } from "../../types/OfficialTypeAlias"; import type { LifetimesConstraint } from "./Lifetimes/LifetimesConstraint"; @@ -11,10 +10,11 @@ type _RootComponentDoc = { methods?: object; events?: object; store?: object; - watch?: Record; + watch?: Record; lifetimes?: LifetimesConstraint; - pageLifetimes?: Partial; + pageLifetimes?: Partial | Partial; externalClasses?: string[]; + export?: () => void; // behaviors 'wx://component-export' 使用 }; // 验证key是否合法 @@ -24,32 +24,20 @@ type _Validator> = [ErrKeys] exten /** * RootComponent Api 返回的类型 * ```ts - * RootComponentDoc = { - * isPage?: true; + * isPage?: boolean; * properties?: object; * data?: object; - * store?:object; * computed?: object; * customEvents?: object; * methods?: object; * events?: object; + * store?: object; + * watch?: Record; + * lifetimes?: LifetimesConstraint; + * pageLifetimes?: Partial | Partial; + * export?:AnyFunction + * externalClasses?: string[]; * }; * ``` */ export type RootComponentDoc = _RootComponentDoc> = O; - -type _RootPageDoc = { - isPage?: boolean; - properties?: object; - data?: object; - computed?: object; - customEvents?: object; - methods?: object; - events?: object; - store?: object; - watch?: Record; - lifetimes?: LifetimesConstraint; - pageLifetimes?: Partial; -}; - -export type RootPageDoc = _RootPageDoc> = O; diff --git a/src/api/RootComponent/Watch/test/WtachInject.test.ts b/src/api/RootComponent/Watch/test/WtachInject.test.ts index 2b7d3af..1928537 100644 --- a/src/api/RootComponent/Watch/test/WtachInject.test.ts +++ b/src/api/RootComponent/Watch/test/WtachInject.test.ts @@ -1,8 +1,8 @@ import { Checking, type Test } from "hry-types"; import { RootComponent } from "../.."; - /** * watch 只能监控 注入的store字段 + * 注入文件 https://github.com/missannil/annil/blob/main/src/api/InstanceInject/inject.ts */ RootComponent()({ watch: { diff --git a/src/api/RootComponent/index.ts b/src/api/RootComponent/index.ts index 4250d75..7abd92b 100644 --- a/src/api/RootComponent/index.ts +++ b/src/api/RootComponent/index.ts @@ -57,7 +57,7 @@ type RootComponentOptions< & StoreOption & ComputedOption & PageLifetimesOption - & LifetimesOption + & LifetimesOption & WatchOption< & ComputedDoc & Required @@ -65,7 +65,7 @@ type RootComponentOptions< & StoreDoc & IInjectStore > - & Partial> + & Partial> & ObserversOption< & ComputedDoc & Required diff --git a/src/api/SubComponent/SubData/SubDataConstraint.ts b/src/api/SubComponent/SubData/SubDataConstraint.ts index e1ccf54..c4c6717 100644 --- a/src/api/SubComponent/SubData/SubDataConstraint.ts +++ b/src/api/SubComponent/SubData/SubDataConstraint.ts @@ -1,6 +1,6 @@ /** * 子组件Data字段约束 - * @returns 当剩余对象不为空对象,约束对象的key为剩余key,类型为剩余key对应的文档类型或函数返回类型。 + * @returns 当剩余对象不为空对象,约束对象的key为剩余key,类型为剩余key对应的文档类型. */ export type SubDataConstraint< TComponentDoc extends object, diff --git a/src/api/SubComponent/SubInherit/SubInheritConstraint.ts b/src/api/SubComponent/SubInherit/SubInheritConstraint.ts index 66769a5..b833fce 100644 --- a/src/api/SubComponent/SubInherit/SubInheritConstraint.ts +++ b/src/api/SubComponent/SubInherit/SubInheritConstraint.ts @@ -5,7 +5,7 @@ import type { ComponentDoc } from "../../DefineComponent/ReturnType/ComponentDoc type WXMLSign = "wxml"; /** - * 子组件inherit字段约束,key为子组件所需key,根组件数据的key或`WXMLSign`。要求根数据类型为子数据类型的子类型。当key的值来自wxml(循环产生的子数据等情况)时用`WXMLSign`表示。 + * 子组件inherit字段约束,key为构建组件所需的properties字段,类型为根组件数据的key或`WXMLSign`。要求根数据类型为子数据类型的子类型。当key的值来自wxml(循环产生的子数据等情况)时用`WXMLSign`表示。 * @returns object */ export type InheritConstraint = { diff --git a/src/api/SubComponent/SubInstance/test/normal.test.ts b/src/api/SubComponent/SubInstance/test/normal.test.ts index 0793236..87637db 100644 --- a/src/api/SubComponent/SubInstance/test/normal.test.ts +++ b/src/api/SubComponent/SubInstance/test/normal.test.ts @@ -43,9 +43,6 @@ SubComponent()({ data: { aaa_str: "str", }, - store: { - _aaa_SubReactive: () => 123, - }, computed: {}, methods: { aaa_SubM() {}, @@ -68,7 +65,6 @@ SubComponent()({ Cnum: number; // 自身Data aaa_str: "str"; - _aaa_SubReactive: 123; } & IInjectData >, Test.Pass diff --git a/src/api/SubComponent/SubLifetimes/SubLifetimesOption.ts b/src/api/SubComponent/SubLifetimes/SubLifetimesOption.ts index c1dc9c1..0699e68 100644 --- a/src/api/SubComponent/SubLifetimes/SubLifetimesOption.ts +++ b/src/api/SubComponent/SubLifetimes/SubLifetimesOption.ts @@ -1,3 +1,3 @@ -import type { WMCompLifetimes } from "../../../types/OfficialTypeAlias"; +import type { LifetimesOption } from "../../RootComponent/Lifetimes/LifetimesOption"; -export type SubLifetimesOption = Partial; +export type SubLifetimesOption = LifetimesOption; diff --git a/src/api/SubComponent/SubStore/SubDataConstraint.ts b/src/api/SubComponent/SubStore/SubStoreConstraint.ts similarity index 100% rename from src/api/SubComponent/SubStore/SubDataConstraint.ts rename to src/api/SubComponent/SubStore/SubStoreConstraint.ts diff --git a/src/api/SubComponent/SubStore/SubStoreOption.ts b/src/api/SubComponent/SubStore/SubStoreOption.ts index 418ac8c..0793c73 100644 --- a/src/api/SubComponent/SubStore/SubStoreOption.ts +++ b/src/api/SubComponent/SubStore/SubStoreOption.ts @@ -1,6 +1,6 @@ import type { V } from "hry-types"; -export type SubStoreOption = { +export type SubStoreOption = { /** * 全局响应式数据字段,全局store对应数据变化实例对应数据自动setData。 * 约束为组件properties字段去除inherit和data的剩余字段和内部字段 @@ -19,7 +19,8 @@ export type SubStoreOption user.name, // @ts-expect-error 2 与 data 字段重复 aaa_num: () => user.age, - // @ts-expect-error 3 内部字段前缀错误 - _num: () => user.age, - // @ts-expect-error 4 超出约束字段 + // @ts-expect-error 3 超出约束字段 num: () => user.age, }, }); diff --git a/src/api/SubComponent/SubStore/test/normal.test.ts b/src/api/SubComponent/SubStore/test/normal.test.ts index 08c108b..277d877 100644 --- a/src/api/SubComponent/SubStore/test/normal.test.ts +++ b/src/api/SubComponent/SubStore/test/normal.test.ts @@ -19,7 +19,17 @@ SubComponent<{}, DocA>()({ aaa_str: () => user.name, aaa_num: () => user.age, - // 运行内部字段(暂定) - _aaa_ddd: () => user.age, + // 内部字段 + // _aaa_ddd: () => user.age, + }, +}); + +SubComponent<{}, DocA, "a">()({ + store: { + aaaA_str: () => user.name, + + aaaA_num: () => user.age, + // 内部字段 + // _aaaA_ddd: () => user.age, }, }); diff --git a/src/api/SubComponent/SubWatch/test/WatchProperties.test.ts b/src/api/SubComponent/SubWatch/test/WatchProperties.test.ts new file mode 100644 index 0000000..5709d46 --- /dev/null +++ b/src/api/SubComponent/SubWatch/test/WatchProperties.test.ts @@ -0,0 +1,161 @@ +import { Checking, type Test } from "hry-types"; +import type { ReadonlyDeep } from "hry-types/src/Any/ReadonlyDeep"; +import { type DetailedType, RootComponent, SubComponent } from "../../../.."; +import type { OptionalType } from "../../../RootComponent/Properties/PropertiesConstraint"; +import { + type Mock_Cart, + mock_requiredTypes, + mock_requiredUnion, + type Mock_User, +} from "../../../RootComponent/Properties/test/normalRequired.test"; + +const mock_optional = { + optional_num: { + type: Number, + value: 123, + }, + optional_gender: { + type: String as DetailedType<"male" | "female">, + value: "male" as const, + }, + optional_obj: { + type: Object as DetailedType, + value: { + id: "id", + name: "name", + age: 20, + }, + }, + optional_objOrNull: { + type: Object as DetailedType, // 可选类型定义null默认值方可为null + value: null, + }, +} satisfies Record; + +const rootComponent = RootComponent()({ + properties: { + ...mock_requiredTypes, + ...mock_requiredUnion, + ...mock_optional, + }, +}); + +type Root = typeof rootComponent; + +/** + * watch properties字段 深度只读, 必传对象字段newValue去除null + */ +SubComponent()({ + watch: { + // 必传单一字段 + str(newValue, oldValue) { + Checking; + + Checking; + }, + num(newValue, oldValue) { + Checking; + + Checking; + }, + bool(newValue, oldValue) { + Checking; + + Checking; + }, + arr(newValue, oldValue) { + newValue; // readonly ReadonlyDeep[] + + oldValue; // readonly ReadonlyDeep[] + + // ts中很多不是错误的错误,写 typeof newValue 结果是错误 + Checking; + + // ts中很多不是错误的错误,写 readonly ReadonlyDeep[] 结果是正确的 + Checking[], Test.Pass>; + }, + obj(newValue, oldValue) { + Checking; + + Checking; + }, + tuple(newValue, oldValue) { + Checking; + + Checking; + }, + union_str(newValue, oldValue) { + Checking<"male" | "female", typeof newValue, Test.Pass>; + + Checking<"male" | "female", typeof oldValue, Test.Pass>; + }, + union_num(newValue, oldValue) { + Checking<0 | 1 | 2, typeof newValue, Test.Pass>; + + Checking<0 | 1 | 2, typeof oldValue, Test.Pass>; + }, + union_bool(newValue, oldValue) { + Checking; + + Checking; + }, + union_arr(newValue, oldValue) { + Checking; + + Checking; + }, + union_obj(newValue, oldValue) { + Checking, typeof newValue, Test.Pass>; + + Checking, typeof oldValue, Test.Pass>; + }, + // 必传多类型联合 + union_str_num_bool(newValue, oldValue) { + Checking; + + Checking; + }, + union_literalStr_Literalnum(newValue, oldValue) { + Checking<0 | 1 | 2 | "male" | "female", typeof newValue, Test.Pass>; + + Checking<0 | 1 | 2 | "male" | "female", typeof oldValue, Test.Pass>; + }, + union_mockUser_num(newValue, oldValue) { + Checking, typeof newValue, Test.Pass>; + + Checking, typeof oldValue, Test.Pass>; + }, + // 可选字段 + optional_gender(newValue, oldValue) { + Checking<"male" | "female", typeof newValue, Test.Pass>; + + Checking<"male" | "female", typeof oldValue, Test.Pass>; + }, + optional_num(newValue, oldValue) { + Checking; + + Checking; + }, + optional_obj(newValue, oldValue) { + Checking, typeof newValue, Test.Pass>; + + Checking, typeof oldValue, Test.Pass>; + }, + // 对象的二段key + "optional_obj.age"(newValue, oldValue) { + Checking; + + Checking; + }, + "optional_obj.**"(newValue, oldValue) { + Checking, typeof newValue, Test.Pass>; + + Checking, typeof oldValue, Test.Pass>; + }, + "optional_obj.id"(newValue, oldValue) { + Checking; + + Checking; + }, + }, +}); diff --git a/src/api/SubComponent/index.ts b/src/api/SubComponent/index.ts index a44bc4a..507e8ee 100644 --- a/src/api/SubComponent/index.ts +++ b/src/api/SubComponent/index.ts @@ -32,7 +32,7 @@ import type { SubMethodsOption } from "./SubMethods/SubMethodsOption"; import type { SubObserversOption } from "./SubObservers/SubObserversOption"; import type { SubPageLifetimesOption } from "./SubPageLifetimes/SubPageLifetimesOption"; import type { CreateSubComponentDoc } from "./SubReturnType/CreateSubComponentDoc"; -import type { SubStoreConstraint } from "./SubStore/SubDataConstraint"; +import type { SubStoreConstraint } from "./SubStore/SubStoreConstraint"; import type { SubStoreOption } from "./SubStore/SubStoreOption"; import type { SubWatchOption } from "./SubWatch/SubWatchOption"; @@ -63,8 +63,7 @@ type Options< > & SubStoreOption< TSubStore, - Exclude, - Prefix + Exclude > & SubComputedOption< TSubComputed, @@ -84,7 +83,7 @@ type Options< & SubStoreDoc & IInjectStore > - & Partial> + & Partial> & SubObserversOption< & SubComputedDoc & SubDataDoc diff --git a/src/behaviors/BComputedAndWatch/isEqual.ts b/src/behaviors/BComputedAndWatch/isEqual.ts index daffd57..5f6a98c 100644 --- a/src/behaviors/BComputedAndWatch/isEqual.ts +++ b/src/behaviors/BComputedAndWatch/isEqual.ts @@ -11,7 +11,8 @@ function isSameSize(a: object, b: object) { // 定义一个辅助函数,用于判断两个函数的代码是否相同 function isSameCode(a: Function, b: Function) { - return a.toString() === b.toString(); + // 去除空格比较函数字符串 + return a.toString().split(" ").join("") === b.toString().split(" ").join(""); } // 定义一个辅助函数,用于判断两个日期的时间戳是否相同