封面图作者:镜子222333
很早就听过electron
和reactnaive
,可以让前端的同学来使用他们熟悉的web前端技术栈来分别开发pc客户端和移动客户端;再后来小程序火了起来后,也有不少团队开始做起了多端共用一套代码这样的理想化框架,比如滴滴的chameleon
和京东的Taro
,但是对于这些新’玩具’一直是停留在知道层面,并没有接触,趁着大四最后的一个假期, 想着接触一下;然后发现了一款叫scriptable
的ios/macos上的的app;可以用js来实现对该应用在ios桌面组件的自定义;有点类似小程序那样,微信封装一些底层设备的操作暴露给上层,然后由我们来利用这些api来做二次开发,所以最近以这个为头,尝试了第一次的’跨端’开发,并记录一下第一次尝试。
写在前面
- 首先需要一台升级到
ios14
的ios||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
- 在apple的js环境中,
- 附上一个自己写的计算天数的脚本
// 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);