雨化田

又一个无名小格

JSBox-Component教程——写一个TabLayout

2024-02-13

什么是 JSBox?

JSBox 是一个可以用来运行 JavaScript 脚本的 iOS 应用,你可以通过他来执行标准的 JavaScript 脚本。

这种执行不是指跑在浏览器上,而是执行在一个完全原生的环境,效率很高。

并且提供了很多 iOS 原生的接口,这意味着你可以通过他做很多事情,包括但不限于:

  • 写一个用来查询汇率的脚本;

  • 写一个用于计算小费的脚本;

  • 通过接口实现一个小小的应用,提供丰富的界面;

  • 写一个文本收藏工具,用于收藏常用的文字;

  • 写一个下载 Twitter 视频的小工具;

——引用自少数派:JSBox: 一个创造工具的工具

什么是 JSBox-Component?

JSBox-Component 是一个在 JSBox 中实现 ui 组件化的库,可以将页面拆分为不同的组件从而复用。

此教程将带你快速上手JSBox-Component这个库,提高开发效率😎

结果展示

IMG_0011.png

{
  type: TabLayout,
  props: {
    index: 1,
    tabs: [
      {
        title: "Home",
        icon: "102"
      },
      {
        title: "Favorite",
        icon: "061"
      },
      {
        title: "Settings",
        icon: "002"
      }
    ]
  },
  layout(make, view) {
    make.bottom.right.left.equalTo(view.super.safeArea);
    make.height.equalTo(50);
  },
  events: {
    onTabChanged: (index, tab) => {
      console.log(index, tab);
    }
  }
}

通过以上组件对象在屏幕上创建一个 TabLayout ,并拥有以下功能:

  • 传入一个 tabs 属性指定每一个 tab 的文本和图标

  • 可以通过传入 index 属性指定一开始选中的 tab

  • 可以通过传入 tabColortabSelectedColor 属性指定 tab 未选中和选中时的颜色

  • 通过 onTabChanged 事件可以监听当前选中 tab 是否改变

  • 通过更改 index 属性可以改变当前选中的 tab

创建项目

首先从 Release 中下载最新版本的 component.js

PixPin_2024-02-16_17-32-25.webp

创建一个空白项目,新建一个 lib 文件夹,把 component.js 放到里面

接着创建一个 components 文件夹,里面存放该项目的自定义组件

jsbox-tablayout-demo
│
│  main.js
│  config.json
│
├─assets
│
├─components
│
├─lib
│      component.js
│
├─scripts
│
└─strings

编写组件(显示部分)

components 目录下新建 TabLayout.js

首先从 component.js 导入 defineComponent 方法,这个方法可以用来定义一个组件

const { defineComponent } = require("../lib/component");

接着导出这个组件

const { defineComponent } = require("../lib/component");

module.exports = defineComponent();

现在就可以开始编写组件了

defineComponent 传入一个对象,可以包含以下属性:

name

name 属性就是这个组件的名字,和文件名保持一致即可

props

props 属性定义这个组件接收哪些属性值,这里就是 index, tabs, tabColor, tabSelectedColor 这四项,后面跟上这些属性的默认值。

index 默认为 0,代表默认第一个 tab 被选中

events

events 是一个数组,包含了该组件的事件名,这里就只有 onTabChanged 事件。

methods

methods 定义了组件在页面上的实例可以被调用的方法,这里只有一个 changeTab 方法,用来改变选中的 tab,具体的函数之后再写。

watch

watch 可以监听属性值的变化,这里暂时用不着。

const { defineComponent } = require("../lib/component");

module.exports = defineComponent({
  name: "TabLayout",
  props: {
    index: 0, // 默认选中第一项
    tabs: [],
    tabColor: $color("gray"), // 默认未选中色为灰色
    tabSelectedColor: $color("black") // 默认选中色为黑色
  },
  events: ["onTabChanged"],
  methods: {
    changeTab() {
      // 留位置给之后写具体逻辑
    }
  },
});

编写 render 方法

render 方法是一个组件的核心,它将输入的组件对象转换成一个 JSBox 原生控件对象并返回,这样才能够在页面上渲染出来组件。

首先我们写一个普通的 TabLayout ,是这样子的

{
  type: "matrix",
  props: {
    columns: 1,
    itemHeight: 60,
    spacing: 0,
    scrollEnabled: false,
    bgcolor: $color("clear"),
    template: [
      {
        type: "image",
        props: { id: "icon", bgcolor: $color("clear") },
        layout(make, view) {
          make.centerX.equalTo(view.super);
          make.width.height.equalTo(25);
          make.top.inset(7);
        }
      },
      {
        type: "label",
        props: { id: "title", font: $font(10) },
        layout(make, view) {
          make.centerX.equalTo(view.prev);
          make.bottom.inset(13);
        }
      }
    ],
    data: [{
      icon: {
        icon: $icon(102, $color("black"), $size(50, 50))
      },
      title: {
        text: "首页",
        textColor: $color("black"),
      }
    }]
  }
}

其实就是一个 matrix ,每个格子有一个 imagelabel 控件,用于显示图标和文本,现在让我们把这个 matrix 封装进 render 方法里面。

首先每行的格子数也就是 columns 属性要变为传入的 tabs 属性的长度。

可以通过 this 访问到这个组件对象,用 this.props.tabs 就可以拿到 tabs 这个属性的值了,如果有传入则会得到传入的值,没有就会得到前面定义的默认值。

render() {
    return {
      type: "matrix",
      props: {
        columns: this.props.tabs.length,
        itemHeight: 60,
        spacing: 0,
        scrollEnabled: false,
        bgcolor: $color("clear"),
        template: [
          ...
        ],
        data: ...
      }
    }
  }

然后将 tabs 转换为 matrixdata 属性

render() {
  // map函数内this的指向会改变,因此先把要props提取出来
  const props = this.props;
  const data = props.tabs.map(function (item, index) {
    const color = index === props.index ? props.tabSelectedColor : props.tabColor;
    return {
      icon: {
        icon: $icon(
          item.icon,
          color,
          $size(50, 50)
        )
      },
      title: {
        text: item.title,
        textColor: color,
      }
    };
  });
  return {
    type: "matrix",
    props: {
      columns: 1,
      itemHeight: 60,
      spacing: 0,
      scrollEnabled: false,
      bgcolor: $color("clear"),
      template: [
        {
          type: "image",
          props: { id: "icon", bgcolor: $color("clear") },
          layout(make, view) {
            make.centerX.equalTo(view.super);
            make.width.height.equalTo(25);
            make.top.inset(7);
          }
        },
        {
          type: "label",
          props: { id: "title", font: $font(10) },
          layout(make, view) {
            make.centerX.equalTo(view.prev);
            make.bottom.inset(13);
          }
        }
      ],
      data: data
    }
  }
}

至此组件的显示就完成了。

渲染组件

main.js 中,从 component.js 导入 render 函数,再导入刚刚编写的 TabLayout 组件。

const { render } = require("./lib/component");

const TabLayout = require("./components/TabLayout");

使用 render 函数创建一个页面,使用方法和 $ui.render 一致,不同的是这个函数能够识别自定义组件。

views 中写入组件对象,组件的 type 设置为刚导入的 TabLayout ,再编写一下属性值和布局。

render({
  views:[{
    type: TabLayout,
    props: {
      index: 1,
      tabs: [
        {
          title: "Home",
          icon: "102"
        },
        {
          title: "Favorite",
          icon: "061"
        },
        {
          title: "Settings",
          icon: "002"
        }
      ]
    },
    layout(make, view) {
      make.bottom.right.left.equalTo(view.super.safeArea);
      make.height.equalTo(50);
    }
  }]
});

运行,就会看见页面上出现了刚刚编写的组件。

IMG_0011.png

改变上面代码中的 indextabs 的值,重启脚本,组件也会对应发生变化。

优化组件(交互部分)

下面让我们来实现组件的交互,即点击 tab 会切换,并且可以通过外部代码控制此组件。

组件内部状态管理

可以看到,该组件的所有变化都是通过 index 定义的,只要 index 改变,组件的显示就应该跟着改变。因此 index 属性表示了该组件的内部状态。

当用户点击 tab,或是外部代码控制组件切换 tab,实质上应改变组件内部的 index 属性值,组件应该监听 index 的变化从而在页面上做出变化。

开始实现

matrix 添加一个点击事件。触发时应将 index 变为被点击的 tab 的 index

events: {
  didSelect(_, indexPath) {
    this.props.index = indexPath.item;
  }
}

编写 changeTab 事件,设置 index 为传入的值。

methods: {
  changeTab(tabIndex) {
    this.props.index = tabIndex;
  }
}

再添加一个 watch 属性,监听 index 的变化。当属性发生变化时会触发对应的函数,传入新值和旧值。

通过this.view 拿到该组件在页面上的实例,将新的 index 对应的 tab 的颜色变为 tabSelectedColor 的颜色值,将旧的 tab 颜色变为 tabColor 的值。

接着再通知 onTabChanged 事件,告诉它当前选中 tab 已更改。

watch: {
  index(newIndex, oldIndex) {
    const data = this.view.data;
    data[newIndex].title.textColor = this.props.tabSelectedColor;
    data[newIndex].icon.icon = $icon(
      this.props.tabs[newIndex].icon,
      this.props.tabSelectedColor,
      $size(50, 50)
    );
    data[oldIndex].title.textColor = this.props.tabColor;
    data[oldIndex].icon.icon = $icon(
      this.props.tabs[oldIndex].icon,
      this.props.tabColor,
      $size(50, 50)
    );
    this.view.data = data;
    this.events.onTabChanged(newIndex, this.props.tabs[newIndex]);
  }
}

至此整个组件的编写工作就结束了。

现在再运行一遍项目,发现点击 tab 会切换了。

通过外部代码修改选中 tab

现在我们来实现一个页面创建 3 秒后使第二个 tab 被选中的效果。

要实现此效果就是要改变组件的内部状态,即 index 属性。

首先给组件加上属性 id"tablayout1",然后导入 get 函数。

get 函数的用法

get 函数的用法和 $("id") 一致,都是通过 id 获取组件在页面上的实例,但此函数可以识别自定义组件。

获取实例后可以直接修改属性值,也可以调用组件 methods 里的方法。

const { render, get } = require("./lib/component");

setTimeout(() => {
  // 直接修改属性值
  get("tablayout1").index = 1;

  // 调用方法实现
  // get("tablayout1").changeTab(1);
}, 3000);

Q&A

哪里可以获取 this 对象?

  • methods 属性内

  • watch 属性内

  • render 方法内

  • render 方法返回的控件对象的 events

this 对象包括哪些属性?

props, events, methods, view

如何手动转换一个组件

使用 trans 函数

const { trans } = require("./lib/component");

const TabLayout = require("./components/TabLayout");

$ui.render({
  views: [
    trans({
      type: TabLayout,
      props: {
        index: 1,
        tabs: [
          {
            title: "Home",
            icon: "102"
          }
        ]
      },
      layout(make, view) {
        make.bottom.right.left.equalTo(view.super.safeArea);
        make.height.equalTo(50);
      }
    })
  ]
});

注意事项

  1. render 方法的返回结果必须是一个控件对象,如果组件是由多个控件组成的,请包裹至一个 view 控件中

  2. views 属性的值会自动加到根控件的 views 后面

结语

至此,你已经对 jsbox-component 有了一个全面的了解,其他的一些特性和用法可以到 components 文件夹下查看示例组件。

上一次使用 JSBox 这个软件还是在三年前,三年前我为 JSBox 写过一个名为 AndStyle 的库,也是可以使用各种组件。但由于当时能力有限,对前端不甚了解,因此存在诸多不完善的地方。这回终于把从前一直想实现但无从下手的功能开发出来了,也算了结一个遗憾。

对的,我就是那个在 18-20 年左右写了一堆脚本的人,哈哈哈,以前我的 id 还叫 Hhhd,不知道有没有人记得我🤣

当时我天天在 JSBox 的 tg 群水群,还认识了不少大佬,比如 Ryan(记得是一个北理大佬,很 nb)、N 大(英语很好的一个北外佬)、Fndroid(后来我才知道这位原来是 cfw 作者😲)

本教程的全部代码在 components/TabLayout.js