记一次跨平台开发

封面图作者:镜子222333

很早就听过electronreactnaive,可以让前端的同学来使用他们熟悉的web前端技术栈来分别开发pc客户端和移动客户端;再后来小程序火了起来后,也有不少团队开始做起了多端共用一套代码这样的理想化框架,比如滴滴的chameleon和京东的Taro,但是对于这些新’玩具’一直是停留在知道层面,并没有接触,趁着大四最后的一个假期, 想着接触一下;然后发现了一款叫scriptable的ios/macos上的的app;可以用js来实现对该应用在ios桌面组件的自定义;有点类似小程序那样,微信封装一些底层设备的操作暴露给上层,然后由我们来利用这些api来做二次开发,所以最近以这个为头,尝试了第一次的’跨端’开发,并记录一下第一次尝试。

写在前面

  • 首先需要一台升级到ios14ios||macos||ipados的设备
  • 在设备上下载scriptable
  • 打开软件即可开始书写自己的脚本啦

关于scriptable

软件说明

  • 这个软件做的事就是封装了ios的底层一些api
  • 然后我们用软件提供的api来定制该软件创建的组件所显示的内容
  • 需要注意的是使用的是apple自己的js引擎,但支持ES6
  • 其次因为只是内嵌了js引擎,所有没有浏览器的那些api

开发

  • 打开app,点击右上角加号创建一个新脚本
  • 在创建的脚本文件中,直接开始书写即可

查看效果

  • 在桌面添加该软件的小组件
  • 编辑该小组件,在script中选择我们的脚本
  • 回到桌面就可以查看效果了

代码演示

  • 写一个Hello World
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // 创建一个小组件
    let w=new ListWidget()
    // 设置组件背景颜色
    w.backgroundColor=new Color("#fff")
    // 添加组件内显示的文本
    let textNode=w.addText("Hello World")
    // 在组件内部居中显示文本
    textNode.centerAlignText()
    // 设置文本的颜色
    textNode.textColor=new Color("#000")
    // 渲染组件
    Script.setWidget(w)
    // 通知系统脚本执行完成
    Script.complete()

关于pc开发小组件

  • 软件本身的编辑环境和调试其实蛮方便,但是因为手机和pad的打字输入体验不行,所以如果没有mac的话,想在电脑开发就需要借助一些其他手段
  • 安利一个社区的方案:im3x-dev
  • 因为文档内部代码封装的api写的不是非常友好,写一点开发中的食用指南

食用指南

  • 开发文件:「源码」小组件示例.js内部是一个Widget类,只需要在他提供的class内部编写对应的逻辑函数即可
  • 文件默认本身会包含几个函数,以及提供一些函数,可以做一些操作
    • constructor:初始化组件的一些基本信息,以及注册脚本在软件和用户交互的一些设置
    • render:判断组件大小渲染不同的组件
    • renderSmall:小尺寸组件显示逻辑,参数的data是render函数中请求后拿到的数据
    • renderMedium:中尺寸组件显示逻辑,参数的data是render函数中请求后拿到的数据
    • renderLarge:大尺寸组件显示逻辑,参数的data是render函数中请求后拿到的数据
    • getData:请求数据的函数
  • 定一些自己的函数:写在class里面,然后在其他地方通过this.xxx调用即可
  • 注册一些让用户点击的然后进行一些交互的事件:this.registerAction('显示文本',对应操作的函数)

最后

  • 开发过程中有遇到一个小坑:
    • 在apple的js环境中,new Date()时,如果要传入日期,其格式必须为xxxx/xx/xx,而不是V8那样的xxxx-xx-xx
  • 附上一个自己写的计算天数的脚本
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    // Variables used by Scriptable.
    // These must be at the very top of the file. Do not edit.
    // icon-color: orange; icon-glyph: comments;
    //
    // iOS 桌面组件脚本 @「小件件」
    // 开发说明:请从 Widget 类开始编写,注释请勿修改
    // https://x.im3x.cn
    //

    // 添加require,是为了vscode中可以正确引入包,以获得自动补全等功能
    if (typeof require === 'undefined') require = importModule


    const { Base } = require("./「小件件」开发环境")
    // const { DmYY: Base } = require("./DmYY.js")



    // @组件代码开始
    class Widget extends Base {
    /**
    * 传递给组件的参数,可以是桌面 Parameter 数据,也可以是外部如 URLScheme 等传递的数据
    * @param {string} arg 自定义参数
    */
    constructor(arg) {
    super(arg)
    this.name = '示例小组件'
    this.desc = '「小件件」—— 原创精美实用小组件'
    // 注册操作菜单
    if (config.runsInApp) {
    this.registerAction("设置文本", this.actionSetText)
    this.registerAction("设置时间", this.actionSetDate)
    }
    }
    // 设置文本
    async actionSetText() {
    let getText = new Alert()
    getText.title = "设置组件显示文本"
    getText.message = "请输入组件要显示的文本内容"
    getText.addTextField("输入显示文本", this.settings['text'])
    // 增加按钮
    getText.addAction("确定")
    getText.addCancelAction("取消")
    await getText.presentAlert()
    let inputText = await getText.textFieldValue(0)
    this.settings['text'] = inputText
    // 保存设置
    this.saveSettings()
    }
    // 设置时间
    async actionSetDate() {
    console.log("设置时间")
    try {
    let dp = await new DatePicker()
    let selectDate = await dp.pickDate()
    // ios只能解析 xxxx/xx/xx格式的日期
    let day = `${selectDate.getFullYear()}/${selectDate.getMonth() + 1}/${selectDate.getDate()}`
    this.settings['day'] = day
    this.saveSettings()
    } catch (error) {
    console.log("请选择时间")
    }
    }
    /**
    * 渲染函数,函数名固定
    * 可以根据 this.widgetFamily 来判断小组件尺寸,以返回不同大小的内容
    */
    async render() {
    // 请求接口
    const data = await this.getData()
    switch (this.widgetFamily) {
    case 'large':
    return await this.renderLarge(data)
    case 'medium':
    return await this.renderMedium(data)
    default:
    return await this.renderSmall(data)
    }
    }
    // 渲染背景颜色
    renderBackColor(w) {
    const gradient = new LinearGradient();
    gradient.locations = [0, 1];
    gradient.colors = [new Color("#eec3ee", 1), new Color("#b2c0ed", 1)];
    w.backgroundGradient = gradient;
    }
    // 渲染字体
    renderFontStyle(t, fontSize, fontColor, position) {
    switch (position) {
    case 'center':
    t.centerAlignText()
    break;
    case 'right':
    t.rightAlignText()
    break;
    case 'left':
    t.leftAlignText()
    break;
    default:
    break;
    }
    t.font = Font.lightSystemFont(fontSize)
    t.textColor = new Color(fontColor, 1)
    }
    genTime() {
    var date = new Date(); //1. js获取当前时间
    var min = date.getMinutes(); //2. 获取当前分钟
    date.setMinutes(min + 1); //3. 设置当前时间+10分钟:把当前分钟数+10后的值重新设置为date对象的分钟数
    var y = date.getFullYear();
    var m = (date.getMonth() + 1) < 10 ? ("0" + (date.getMonth() + 1)) : (date.getMonth() + 1);
    var d = date.getDate() < 10 ? ("0" + date.getDate()) : date.getDate();
    var h = date.getHours() < 10 ? ('0' + date.getHours()) : date.getHours()
    var f = date.getMinutes() < 10 ? ('0' + date.getMinutes()) : date.getMinutes()
    var s = date.getSeconds() < 10 ? ('0' + date.getseconds()) : date.getSeconds()
    var formatdate = y + '/' + m + '/' + d + " " + h + ":" + f + ":" + s;
    console.log(formatdate) // 获取10分钟后的时间,格式为yyyy-mm-dd h:f:s
    console.log(new Date(formatdate))
    return formatdate
    }
    /**
    * 渲染小尺寸组件
    */
    async renderSmall(data) {
    console.log("刷新")
    let w = new ListWidget()
    w.refreshAfterDate = new Date(this.genTime())
    let headerT = w.addText(this.settings['text'] ? this.settings['text'] : "默认文本")
    this.renderFontStyle(headerT, 15, "#fff", 'center')
    let start = await this.settings['day']
    let day = start ? Math.ceil((new Date() - new Date(start)) / (1000 * 60 * 60 * 24)) : 1
    const t = w.addText(day.toString())
    t.centerAlignText()
    this.renderFontStyle(t, 60, '#fff', 'center')
    let today = w.addDate(new Date())
    this.renderFontStyle(today, 15, '#fff', 'center')
    this.renderBackColor(w)
    return w
    }
    /**
    * 渲染中尺寸组件
    */
    async renderMedium(data, num = 3) {
    let w = new ListWidget()
    let text = w.addText(this.settings['text'] ? this.settings['text'] : "默认文本")
    this.renderFontStyle(text, 36, "#fff", 'center')
    let day = Math.ceil(parseInt(new Date() - new Date(this.settings['day'])) / (1000 * 60 * 60 * 24))
    // 创建中部布局
    let footerT = w.addText(`${String(day) === 'NaN' || day <= 0 ? "请设置今天之前的时间" : day}`)
    footerT.centerAlignText()
    if (String(day) === 'NaN' || day <= 0) {
    this.renderFontStyle(footerT, 27, "#fff", 'center')
    } else {
    this.renderFontStyle(footerT, 40, "#fff", 'center')
    }
    let today = w.addDate(new Date())
    this.renderFontStyle(today, 15, '#fff', 'center')
    this.renderBackColor(w)
    // w.addSpacer(2)
    return w
    }
    /**
    * 渲染大尺寸组件
    */
    async renderLarge(data) {
    return await this.renderMedium(data, 10)
    }

    /**
    * 获取数据函数,函数名可不固定
    */
    async getData() {
    const api = 'https://x.im3x.cn/v1/test-api.json'
    return await this.httpGet(api, true, false)
    }

    /**
    * 自定义注册点击事件,用 actionUrl 生成一个触发链接,点击后会执行下方对应的 action
    * @param {string} url 打开的链接
    */
    async actionOpenUrl(url) {
    Safari.openInApp(url, false)
    }

    }
    // @组件代码结束

    const { Testing } = require("./「小件件」开发环境")
    await Testing(Widget)