掌握API建模:基本概念和实践
利用Scriptable和百度API:开发你的百度热搜追踪工具
秋天是个适合生活、运动、学习、减肥等各类活动的季节,最近逛到一款应用Scriptable,感觉就像应用届的古天乐一样,刚开始感觉这是一款相貌平平的应用,后面感觉就是帅。先上图让大家感受一下,桌面上的这些组件就是用这应用写的,还有好多帅气的“应用”。最让我觉得好用的一点,是可以在后台定时执行刷新最新内容,这在某种程度上来讲,比真正的App还好。
iOS14发布以后(现在已是iOS16),iPhone、iPad等iOS设备支持用户自定义桌面小物件(又或者称之为小组件、桌面挂件)。利用这个特性,网上出现了许许多多诸如透明时钟、微博热搜、知乎热榜、网易云热评、特斯拉、BMW、名爵、奥迪等等的iPhone桌面。
网上找的一些示意图
Scriptable 使用 Apple 的JavaScriptCore,它默认就支持使用ECMAScript 6标准进行开发构建。这是一款可让您使用 JavaScript 自动化构建 iOS 的应用程序(对于前端开发人员太友好了,直接上手操作)。这应用特别适合有一定动手能力的同学,可以利用现有很多API接口,自行构建各类应用。至于不想动手的同学,现在网上也有有很多免费提供使用的脚本可以直接拿来即用。不想动手的同学回头可以在我的订阅号上发送“Scriptable”,我将推送相关的网站和公众号,大家可以直接拿来就用。个人认为应用的原理不复杂,Android上也是可以实现(我觉得Android应该是有现成类似的应用),回头有空争取做一个Android版。
具体特性如下:
- 支持ES6语法
- 可以使用JavaScript调用一些原生的API
- Siri 快捷方式
- 完善的文档支持(这一点真不认同官方的说法,文档只是罗列概要,很多事情要自己去摸着石头过河)
- 共享表格扩展
- 文件系统继承
- 编辑器的自定义(手机上编程开发的体验棒棒的!)
- 代码样例(推荐访问这个网站Widget Examples – Scriptable – Automators Talk)
- 以及通过x-callback-url和其它APP交互
这是官方的API文档说明——Scriptable Docs – Scriptable Docs
基于上面的特性,可以看得出我们可以基础这些API组合,来实现很多有趣的应用。
这是我自己写的两个组件
接下来,我来给大家讲解如何动手编写自己的第一个桌面组件——百度热搜。
下面请大家先去AppStore下载「Scriptable」这个应用
下载完后,打开Scriptable,进入主界面。
1、创建脚本,完成程序主体构造工作。
在主界面的右上角点击加号,创建一个新的脚本,先写入下面这段代码(这也是官方RandomAPI的基础脚本),完成程序主体构造。
// This script shows a random Scriptable API in a widget. The script is meant to be used with a widget configured on the Home Screen.
// You can run the script in the app to preview the widget or you can go to the Home Screen, add a new Scriptable widget and configure the widget to run this script.
// You can also try creating a shortcut that runs this script. Running the shortcut will show widget.
let data = await getData()//获取数据
let widget = await createWidget()//创建界面
if (config.runsInWidget) {
// The script runs inside a widget, so we pass our instance of ListWidget to be shown inside the widget on the Home Screen.
Script.setWidget(widget)
} else {
// The script runs inside the app, so we preview the widget.
widget.presentMedium()
}
// Calling Script.complete() signals to Scriptable that the script have finished running.
// This can speed up the execution, in particular when running the script from Shortcuts or using Siri.
Script.complete()
async function createWidget() {}
async function getData() {}
Scriptable有几个重要的对象,如Script、config、widget(界面元素)这三个对象是构成组件运行的必要元素。在应用运行时,Scriptable会创建Script是一个全局变量,类似于微信小程度的App对象,属于整体应用入口,这个主要就是设置好Widget对象。
config对象,主要是用于提供组件运行的基础上下文环境配置信息,例如是否在组件运行还是在App上运行,或者基于Siri指令运行?是以大组件、中组件还是小组件展示?
Widget对象则是增加组件UI元素,常用的是ListWidget列表组件。
2、完善热搜数据爬取。
在网上找到百度热搜一个老的API接口,与百度热搜的网页上的数据有点不同,但是妨碍我们使用。具体接口连接如下
这个API接口返回的是JSON格式的报文,因此,我们只要实时拿到这份JSON报文,解析到data->cards[0]->content的前5个数据即可。
// This script shows a random Scriptable API in a widget. The script is meant to be used with a widget configured on the Home Screen.
// You can run the script in the app to preview the widget or you can go to the Home Screen, add a new Scriptable widget and configure the widget to run this script.
// You can also try creating a shortcut that runs this script. Running the shortcut will show widget.
let data = await getData()//获取数据
let widget = await createWidget(data.data.cards[0].content)//创建界面
if (config.runsInWidget) {
// The script runs inside a widget, so we pass our instance of ListWidget to be shown inside the widget on the Home Screen.
Script.setWidget(widget)
} else {
// The script runs inside the app, so we preview the widget.
widget.presentMedium()
}
// Calling Script.complete() signals to Scriptable that the script have finished running.
// This can speed up the execution, in particular when running the script from Shortcuts or using Siri.
Script.complete()
async function createWidget(data) {
let widget = new ListWidget()//先创建ListWidget对象
widget.setPadding(20,10,20,10)//设置好组件与其他应用在桌面上的边距
return widget
}
async function getData() {
let url = "https://top.baidu.com/api/board?platform=wise&tab=realtime"
let req = new Request(url)
return await req.loadJSON()
}
3、填充数据,完成组件开发。
前面两步我们已经拿到了百度热搜返回的数据,接下来要做的就是把数据装载到桌面组件上展示。
// This script shows a random Scriptable API in a widget. The script is meant to be used with a widget configured on the Home Screen.
// You can run the script in the app to preview the widget or you can go to the Home Screen, add a new Scriptable widget and configure the widget to run this script.
// You can also try creating a shortcut that runs this script. Running the shortcut will show widget.
let data = await getData()//获取数据
let widget = await createWidget(data.data.cards[0].content)//创建界面
if (config.runsInWidget) {
// The script runs inside a widget, so we pass our instance of ListWidget to be shown inside the widget on the Home Screen.
Script.setWidget(widget)
} else {
// The script runs inside the app, so we preview the widget.
widget.presentMedium()
}
// Calling Script.complete() signals to Scriptable that the script have finished running.
// This can speed up the execution, in particular when running the script from Shortcuts or using Siri.
Script.complete()
async function createWidget(data){
let widget = new ListWidget()//先创建ListWidget对象
widget.refreshAfterDate = new Date(Date.now()+1000*60*5)//这个是指定多长时间后台重新执行一遍脚本,当前是设置每5分钟刷新一次,至于刷新的速率很大程度上取决于操作系统,如你的电量、内存、网络环境等。
widget.setPadding(20,10,20,10)//设置好组件与其他应用在桌面上的边距
//创建头部展示区域
const header = widget.addStack()//WidgetStack组件相当于HTMl的div
header.size = new Size(0,24)
const headerIcon = header.addImage(await loadImg('https://ss1.baidu.com/6ONXsjip0QIZ8tyhnq/it/u=3779990328,1416553241&fm=179&app=35&f=PNG?w=108&h=108&s=E7951B62A4639D153293A4E90300401B'))
headerIcon.imageSize = new Size(22,22)
header.addSpacer(2)
const headerTitle = header.addText('百度热搜')
headerTitle.textColor = new Color('#0080FF')
headerTitle.font = Font.boldSystemFont(18)
// 当组件设置为2X2时,仅能显示Logo,直接返回头部图标就可以了
if(config.widgetFamily === 'small') {
return widget
}
header.addSpacer()//增加空隙
//创建身体展示区域
const content = widget.addStack()
content.layoutVertically()
content.addSpacer(5)
for(var i=0;i<5;i++){
let d = data[i]
const row = content.addStack()
row.size = new Size(0,22)
const rowSeqText = row.addText(String(i+1))
rowSeqText.font = Font.boldSystemFont(14)
rowSeqText.lineLimit = 1
row.addSpacer(10)
const rowText = row.addText(d.desc==''?d.word:d.desc)
rowText.font = Font.boldSystemFont(14)
rowText.lineLimit = 1
row.addSpacer()
const url = d.appUrl
row.url = url
}
widget.addSpacer()
// 创建底部展示区域,增加一个时间展示,方便我们知道当前的热搜结果是什么时间点的结果
const footer = widget.addStack()
footer.size = new Size(0, 16)
footer.addSpacer()
const DF = new DateFormatter()
DF.dateFormat = 'yyyy-MM-dd HH:mm:ss'
const now = DF.string(new Date())
const footerText = footer.addText(now)
footerText.font = Font.regularSystemFont(14)
footerText.lineLimit = 1
return widget
}
async function getData() {
let url = "https://top.baidu.com/api/board?platform=wise&tab=realtime"
let req = new Request(url)
return await req.loadJSON()
}
async function loadImg (url) {//获取网络图片或者小图标
const req = new Request(url)
return await req.loadImage()
}
4、完成发布及应用。
短短70行不到的代码,完成一个组件的编写工作。点击右下脚的三角形,试运行一遍,看看是否正常。如果正常,则开始在桌面测试组件,选择对应的脚本。完成组件的开发及应用工作。具体如下图操作即可。
添加组件的方法是在桌面空白位长按屏幕,直到出现图3,点击+号,选择Scriptable组件。
添加完桌面组件后,点击新增加的组件,如图3设置,选择刚刚编写的脚本(默认脚本名叫Untitled Script),至此完成,将实时显示百度热搜消息,并且将在约5分钟左右自动刷新最新消息。
1、查看百度热搜页面结构
通过分析页面结构,发现热搜项均包含在class=”item-wrap_2oCLZ”的a标签中,十分简单的结构,直接通过document.querySelectorAll(“.item-wrap_2oCLZ”)取到这些热搜项的数据,然后直接获取outerText,取得相应的文本内容即可。
下面的代码为上一篇的代码。
// This script shows a random Scriptable API in a widget. The script is meant to be used with a widget configured on the Home Screen.
// You can run the script in the app to preview the widget or you can go to the Home Screen, add a new Scriptable widget and configure the widget to run this script.
// You can also try creating a shortcut that runs this script. Running the shortcut will show widget.
let data = await getData()//获取数据
let widget = await createWidget(data.data.cards[0].content)//创建界面
if (config.runsInWidget) {
// The script runs inside a widget, so we pass our instance of ListWidget to be shown inside the widget on the Home Screen.
Script.setWidget(widget)
} else {
// The script runs inside the app, so we preview the widget.
widget.presentMedium()
}
// Calling Script.complete() signals to Scriptable that the script have finished running.
// This can speed up the execution, in particular when running the script from Shortcuts or using Siri.
Script.complete()
async function createWidget(data) {
let widget = new ListWidget()//先创建ListWidget对象
widget.refreshAfterDate = new Date(Date.now()+1000*60*5)//这个是指定多长时间后台重新执行一遍脚本,当前是设置每5分钟刷新一次,至于刷新的速率很大程度上取决于操作系统,如你的电量、内存、网络环境等。
widget.setPadding(20,10,20,10)//设置好组件与其他应用在桌面上的边距
//创建头部展示区域
const header = widget.addStack();//WidgetStack组件相当于HTMl的div
header.size = new Size(0,24);
const headerIcon = header.addImage(await loadImg('https://ss1.baidu.com/6ONXsjip0QIZ8tyhnq/it/u=3779990328,1416553241&fm=179&app=35&f=PNG?w=108&h=108&s=E7951B62A4639D153293A4E90300401B'))
headerIcon.imageSize = new Size(22,22);
header.addSpacer(2);
const headerTitle = header.addText('百度热搜');
headerTitle.textColor = new Color('#0080FF');
headerTitle.font = Font.boldSystemFont(18);
// 当组件设置为2X2时,仅能显示Logo,直接返回头部图标就可以了
if(config.widgetFamily === 'small') {
return widget
}
header.addSpacer();//增加空隙
//创建身体展示区域
const content = widget.addStack();
content.layoutVertically();
content.addSpacer(5);
for(var i=0;i<5;i++){
let d = data[i];
const row = content.addStack();
row.size = new Size(0,22);
const rowSeqText = row.addText(String(i+1));
rowSeqText.font = Font.boldSystemFont(14)
rowSeqText.lineLimit = 1;
row.addSpacer(10);
const rowText = row.addText(d.desc==''?d.word:d.desc);
rowText.font = Font.boldSystemFont(14)
rowText.lineLimit = 1;
row.addSpacer();
const url = d.appUrl;
row.url = url;
}
widget.addSpacer();
// 创建底部展示区域,增加一个时间展示,方便我们知道当前的热搜结果是什么时间点的结果
const footer = widget.addStack()
footer.size = new Size(0, 16)
footer.addSpacer()
const DF = new DateFormatter()
DF.dateFormat = 'yyyy-MM-dd HH:mm:ss'
const now = DF.string(new Date())
const footerText = footer.addText(now)
footerText.font = Font.regularSystemFont(14)
footerText.lineLimit = 1
return widget
}
async function getData() {
let url = "https://top.baidu.com/api/board?platform=wise&tab=realtime"
let req = new Request(url)
return await req.loadJSON()
}
async function loadImg (url) {//获取网络图片或者小图标
const req = new Request(url)
return await req.loadImage()
}
大部份代码不需要改动,我们只是对数据源获取模块进行修改,调整一下对应的数据结构就可以了。
2、修改取数逻辑
由于Scriptable自带WebView浏览器组件,是基于苹果系统自带的WKWebkit内核实现的,支持加载远程页面、本地文件加载,支持JavaScript脚本注入以及请求拦截(高级功能,可以方便的突破很多前端限制,例如自动化脚本、绕过防御、模拟人机交互等)
async function getData(url) {
let wv = new WebView()//自带内置浏览器组件,默认不展示页面
await wv.loadURL(url)//加载远程页面
//定义注入的JS
let js = `
function _amethod(){
var _result = [];
var _a = document.querySelectorAll(".item-wrap_2oCLZ");
for(var i=0;i<_a.length;i++){
_result.push({"title":_a[i].outerText,"href":_a[i].href});
}
return _result;
}
_amethod();
`
let datas = await wv.evaluateJavaScript(js)//执行注入并接收返回结果
return datas
}
3、修改对应的数据结构逻辑。
let widget = await createWidget(data)
。。。。
//数据结构变化,调整对应的组数逻辑k
for(var i=0;i<5;i++){
let d = data[i];
const row = content.addStack();
row.size = new Size(0,22);
var _rows = d.title.split('\n');//百度返回的文本结构问题,简单点处理
const rowSeqText = row.addText(_rows[0]);
rowSeqText.font = Font.boldSystemFont(14)
rowSeqText.lineLimit = 1;
// rowSeqText.shadowRadius = 1;
// rowSeqText.shadowOffset = new Point(1,1);
row.addSpacer(10);
const rowText = row.addText(_rows[1]);
rowText.font = Font.boldSystemFont(14)
rowText.lineLimit = 1;
// rowText.shadowRadius = 2;
// rowText.shadowOffset = new Point(1,1);
row.addSpacer();
const url = d.href;
row.url = url;
}
完整的代码如下:
let data = await getData('https://top.baidu.com/board?platform=pc&sa=pcindex_entry')
let widget = await createWidget(data)
if (config.runsInWidget) {
// The script runs inside a widget, so we pass our instance of ListWidget to be shown inside the widget on the Home Screen.
Script.setWidget(widget)
} else {
// The script runs inside the app, so we preview the widget.
widget.presentMedium()
}
// Calling Script.complete() signals to Scriptable that the script have finished running.
// This can speed up the execution, in particular when running the script from Shortcuts or using Siri.
Script.complete()
async function createWidget(data) {
let widget = new ListWidget();
widget.setPadding(20,10,20,10);
const header = widget.addStack();
header.size = new Size(0,24);
const headerIcon = header.addImage(await loadImg('https://ss1.baidu.com/6ONXsjip0QIZ8tyhnq/it/u=3779990328,1416553241&fm=179&app=35&f=PNG?w=108&h=108&s=E7951B62A4639D153293A4E90300401B'))
headerIcon.imageSize = new Size(22,22);
header.addSpacer(2);
const headerTitle = header.addText('百度热搜');
headerTitle.textColor = new Color('#0080FF');
headerTitle.font = Font.boldSystemFont(18);
widget.refreshAfterDate = new Date(Date.now()+1000*5)
// S: 当组件设置为2X2时,仅能显示Logo,直接Return
if(config.widgetFamily === 'small') {
return widget
}
header.addSpacer();
const content = widget.addStack();
content.layoutVertically();
content.addSpacer(5);
for(var i=0;i<5;i++){
let d = data[i];
const row = content.addStack();
row.size = new Size(0,22);
var _rows = d.title.split('\n');
const rowSeqText = row.addText(_rows[0]);
rowSeqText.font = Font.boldSystemFont(14)
rowSeqText.lineLimit = 1;
// rowSeqText.shadowRadius = 1;
// rowSeqText.shadowOffset = new Point(1,1);
row.addSpacer(10);
const rowText = row.addText(_rows[1]);
rowText.font = Font.boldSystemFont(14)
rowText.lineLimit = 1;
// rowText.shadowRadius = 2;
// rowText.shadowOffset = new Point(1,1);
row.addSpacer();
const url = d.href;
row.url = url;
}
widget.addSpacer();
// 底部容器
const footer = widget.addStack()
footer.size = new Size(0, 16)
footer.addSpacer()
const DF = new DateFormatter()
DF.dateFormat = 'yyyy-MM-dd HH:mm:ss'
const now = DF.string(new Date())
const footerText = footer.addText(now)
footerText.font = Font.regularSystemFont(14)
footerText.lineLimit = 1
return widget
}
async function getData(url) {
let wv = new WebView()
await wv.loadURL(url)
let js = `
function _amethod(){
var _result = [];
var _a = document.querySelectorAll(".item-wrap_2oCLZ");
for(var i=0;i<_a.length;i++){
_result.push({"title":_a[i].outerText,"href":_a[i].href});
}
return _result;
}
_amethod();
`
let datas = await wv.evaluateJavaScript(js)
return datas
}
async function alert(text){
// 创建一个弹窗组件
let alert = new Alert();
// 设置弹窗中显示的content
alert.message = text;
// 向弹窗中加入一个按钮-确定,索引为0
alert.addAction('确定');
// 向弹窗中加入一个按钮-取消,所以为1
alert.addAction('取消');
// 获取弹窗按钮被触发后拿到用户点击的具体某个按钮索引,如果点击确定,response === 0 否则 response === 1
let response = await alert.presentAlert();
}
async function loadImg (url) {
const req = new Request(url)
return await req.loadImage()
}
拷贝代码到Scriptable,执行效果如下。
发现直接在推文上拷贝脚本是无法直接运行的,因为代码块会对空行增加一个空白字符,导致脚本无法运行。大家有需要的话,请在公众号回复「百度热搜」获取可直接使用的脚本。
Scriptable真是一个超级棒的应用,基于开放的API,真的可以简单快速的做出很多有趣的应用出来。近期看着疫情比较紧张,于是动手做了一个疫情实时监控的组件,可以切换城市,实时更新。先上图,下一篇再介绍如何实现。
本文章转载微信公众号@飙猪狂