Scriptable - 用Javascript编写iOS桌面组件

3,144 阅读3分钟

某个工作日的傍晚,临近饭点,随手打开掘金,发现吹弹可爱的iOS桌面组件居然可以用 Javascript 编写!👇
快使用Scriptable自己开发一个iPhone小组件吧

然而 Hello World 是学会了,在编写自己所想的组件过程中发现对其还是了解甚微,官方文档也较为简陋。
因此想把我理解、收获的内容分享出来,便于大家更好地上手编写自己的理想组件。

(建议先到上面那篇文章了解一下用法)

容器

Scriptable 里的容器相当于 Html 的标签元素,想展示内容都必须放在容器中才能呈现,主要有以下:

  • ListWidget:最基本、最外层的容器,相当于 <body>
  • WidgetStack:一个区块,相当于 <div>/<span>
  • WidgetText:文本容器
  • WidgetSpacer:空行元素,可弹性伸缩
  • WidgetImage:图片容器
  • WidgetDate:日期容器,自动刷新(Scriptable 中的其他内容一般是不会实时刷新的,因此日期时间内容需要放在特殊的容器中)

布局

ListWidget

作为整个组件的基本容器,有三种可选的 Size:

需要判定获取时,可通过 config.widgetFamily 获得

WidgetStack

通过 ListWidget 的 addStack() 方法新建。
直接从 ListWidget 上创建得到的是“行”,在此基础上再创建得到的是“列”向的小区块,如下:

const widget = new ListWidget()
const row = widget.addStack()  // 相当于 <div>
const cell = row.addStack()  // 相当于 <span>

一个8行10列表格布局的小例子:

const widget = new ListWidget()
widget.spacing = 2  // 制造点padding

for (let i=0; i<8; i++) {
    const row = widget.addStack()
    row.spacing = 2  // 制造点padding
    for (let j=0; j<10; j++) {
        const cell = row.addStack()
        cell.size = new Size(12, 12)
        cell.cornerRadius = 100  // border-radius: 100%
        cell.backgroundColor = new Color(`#ff${i}6${j}6`)  // 让其颜色跟随i、j序号渐变
    }
}

WidgetSpacer

用于制造空元素,如:
widget.addSpacer(len: number)row.addSpacer(len)
len = 0 时,它就是用于制造弹性空间的利器。
比如我们可以在上面的表格例子中,每行的中间插入一个 WdigetSpacer:

const widget = new ListWidget()
widget.spacing = 2  // 制造点padding

for (let i=0; i<8; i++) {
    const row = widget.addStack()
    row.spacing = 2  // 制造点padding
    for (let j=0; j<10; j++) {
        // ...
        if (j === 4) {
            row.addSpacer()
        }
    }
}

开发框架

单纯在 IDE 上进行代码编写,然后复制粘贴到 Scriptable 上去跑的话,调试过程是非常低级且耗时的,因此这里推荐一个有意思的小件件开发框架

它会将 PC 与 Scriptable 相连,然后我们就可以直接在 VSCode 上进行代码的编写,保存后会自动同步到手机上并自动运行。
具体的操作方法在作者的文档上写的很清楚了,这里不多赘述。

简单说一下其实现思路的解析:

  1. PC 端开启 Node 服务,移动端填入服务器地址(PC 的 ip 地址)后,把文件 POST 给 Node 端
  2. Node 端获取后保存到本地,记录文件更新时间,用 child_process.exec 通过系统命令打开 VSCode 并打开编辑
  3. 客户端 while(1) 不断发送 GET 请求,node 端对比文件最新更新时间与记录时间,更新则发送文件给客户端
  4. 最后,客户端解析、更新文件,执行

这其中有一个有意思的点:
客户端虽然通过 while(1) 重复发送请求,但其获取请求内容后会有 await

const _res = await _req.loadString()

而 node 端在收到请求后的响应是放在一个1s的 setTimeout 里的:

setTimeout(() => {
  // 判断文件时间
  const _time = fs.statSync(WIDGET_FILE).mtimeMs
  if (_time === FILE_DATE) {
    res.send("no").end()
    return
  }
  // 同步
  res.sendFile(WIDGET_FILE)
  console.log('[+] 同步到手机完毕')
  FILE_DATE = _time
}, 1000)

因此是通过 node 端的延迟响应,来达到控制客户端 while 节流的效果。

不足与期望

  • 布局单一,无法获取容器宽高(想做不同屏幕上的布局适配就不可能了)
  • 背景无法透明(设置透明度0,结果是黑色底)
  • 更新频率无法设置(只能设置比默认频率更晚...)
  • 无法数据驱动视图更新(也许可用接口/本地存储)

示例

最后po一下我的b站榜单组件,点击可直接跳转到B站App的相应视频:
(左上角是一个乱入的土味情话小组件,约会前滑开手机看看...)

b站组件源码 👈

别人家的组件 👈