JSBox-Component教程——写一个TabLayout
2024-02-13
什么是 JSBox?
JSBox 是一个可以用来运行 JavaScript
脚本的 iOS 应用,你可以通过他来执行标准的 JavaScript 脚本。
这种执行不是指跑在浏览器上,而是执行在一个完全原生的环境,效率很高。
并且提供了很多 iOS 原生的接口,这意味着你可以通过他做很多事情,包括但不限于:
-
写一个用来查询汇率的脚本;
-
写一个用于计算小费的脚本;
-
通过接口实现一个小小的应用,提供丰富的界面;
-
写一个文本收藏工具,用于收藏常用的文字;
-
写一个下载 Twitter 视频的小工具;
——引用自少数派:JSBox: 一个创造工具的工具
什么是 JSBox-Component?
JSBox-Component 是一个在 JSBox 中实现 ui 组件化的库,可以将页面拆分为不同的组件从而复用。
此教程将带你快速上手JSBox-Component
这个库,提高开发效率😎
结果展示
{
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 -
可以通过传入
tabColor
和tabSelectedColor
属性指定 tab 未选中和选中时的颜色 -
通过
onTabChanged
事件可以监听当前选中 tab 是否改变 -
通过更改
index
属性可以改变当前选中的 tab
创建项目
首先从 Release 中下载最新版本的 component.js
创建一个空白项目,新建一个 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
,每个格子有一个 image
和 label
控件,用于显示图标和文本,现在让我们把这个 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
转换为 matrix
的 data
属性
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);
}
}]
});
运行,就会看见页面上出现了刚刚编写的组件。
改变上面代码中的 index
和 tabs
的值,重启脚本,组件也会对应发生变化。
优化组件(交互部分)
下面让我们来实现组件的交互,即点击 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);
}
})
]
});
注意事项
-
render
方法的返回结果必须是一个控件对象,如果组件是由多个控件组成的,请包裹至一个 view 控件中 -
views
属性的值会自动加到根控件的views
后面
结语
至此,你已经对 jsbox-component
有了一个全面的了解,其他的一些特性和用法可以到 components 文件夹下查看示例组件。
上一次使用 JSBox 这个软件还是在三年前,三年前我为 JSBox 写过一个名为 AndStyle 的库,也是可以使用各种组件。但由于当时能力有限,对前端不甚了解,因此存在诸多不完善的地方。这回终于把从前一直想实现但无从下手的功能开发出来了,也算了结一个遗憾。
对的,我就是那个在 18-20 年左右写了一堆脚本的人,哈哈哈,以前我的 id 还叫 Hhhd,不知道有没有人记得我🤣
当时我天天在 JSBox 的 tg 群水群,还认识了不少大佬,比如 Ryan(记得是一个北理大佬,很 nb)、N 大(英语很好的一个北外佬)、Fndroid(后来我才知道这位原来是 cfw 作者😲)
本教程的全部代码在 components/TabLayout.js