记一次跨平台开发

封面图作者:镜子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
    // 创建一个小组件
    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
  • 附上一个自己写的计算天数的脚本
    // 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)