什么是 JSBox?

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

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

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

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

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

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

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

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

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

什么是 JSBox-Component?

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

:::link-card {url = “https://github.com/Aziteee/jsbox-component”} :::

此教程将带你快速上手 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文件夹,里面存放该项目的自定义组件

Terminal window
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)、Neurogram(狗哥)(英语很好的一个北外佬)、Fndroid(后来我才知道这位原来是 cfw 作者😲)

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