Skip to content

Latest commit

 

History

History
324 lines (287 loc) · 9.57 KB

designIdea.md

File metadata and controls

324 lines (287 loc) · 9.57 KB

组件构建(设计)方案

annil在构建组件(页面)时采用根组件(RootComponent)与子组件(SubComponent)组合的方式。需要注意的是,为了接收外部泛型,RootComponent和SubComponent都是高阶函数,需要两次调用。

  1. 基础组件(baseComp)

    // 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<Gender>, // 必传属性
        age: { // 选传属性
          type: Number as DetailedType<Age>,
          value: 16, // 有字段检测和类型检测
        },
      },
      customEvents: {
        tap: String as DetailedType<Gender>,
      },
      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)

    <!-- index.wxml -->
    <view id="A">
      <baseComp  age="{{baseComp_age}}" gender="{{baseComp_gender}}" tap="baseComp_tap"/>
    </view>
    // 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<Root, $BaseComp>()({ // 切记 两次调用
      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<User>,
      },
      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

    <!-- demo.wxml -->
    <view id="A">
      <baseComp gender="{{baseCompA_gender}}" tap="baseCompA_tap"/>
    </view>
    <view id="B">
      <baseComp gender="{{baseCompInB_gender}}" tap="baseCompInB_tap"/>
    </view>
    import { DefineComponent, type DetailedType, RootComponent } from "annil";
    import type { $BaseComp, Gender } from "path/to/baseComp";
    
    // SubComponent的第三个泛型输入为字段后缀,会以大驼峰形式加在组件前缀后面
    const baseCompA = SubComponent<Root, $BaseComp, "a">()({
      data: {
        baseCompA_gender: "female",
      },
      events: {
        baseCompA_tap(e) {
          const gender = e.detail;
    
          this.rootMethods(gender);
        },
      },
    });
    const baseCompInB = SubComponent<Root, $BaseComp, "inB">()({
      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).

    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选项。

    type Gender = "male" | "female";
    const rootComponent = RootComponent()({
      data: {
        gender: "male" as Gender,
      },
      // 自定义事件字段更便于代码阅读和类型友好
      customEvents: {
        tap: {
          detail: String as DetailedType<Gender>,
          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定义字段做数据改变

    // storeUser.ts
    import { observable } from "mobx";
    
    type User = { name: string; age: number };
    
    export const storeUser = observable<User>({
      name: "zhao",
      age: 20,
    });
    import { DefineComponent, type DetailedType, RootComponent } from "annil";
    import { storeUser, type User } from "storeUser";
    
    const rootComponent = RootComponent()({
      properties: {
        user: Object as DetailedType<User>,
      },
      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字段的类型,从而报错。

import { DefineComponent, type DetailedType, RootComponent } from "annil";
import type { $BaseComp, Age, Gender } from "path/to/baseComp";
const baseComp = SubComponent<Root, $BaseComp>()({
  // 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字段报错。