记一次跨平台开发
封面图作者:镜子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
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
- 在apple的js环境中,
- 附上一个自己写的计算天数的脚本
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)
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!