JSBox 学习路线:从入门到实践 (ChatGPT版)
前言
这份学习材料根据我们之前互动学习的成果,并结合审阅意见修订而成。它旨在提供一个更准确、更详尽、更符合当前 JSBox API 最佳实践的系统化学习路径。JSBox 是一款强大的 iOS 脚本工具,它允许你使用 JavaScript 语言与 iOS 原生功能深度交互,实现自动化、自定义 UI 和各种实用工具。
本材料将按照以下八个主要阶段展开:
-
快速开始: 了解 JSBox 的基本哲学和代码运行方式。
-
基础接口: 掌握与应用、设备、网络和数据存储相关的核心 API。
-
构建界面: 学习如何使用 JavaScript 定义和布局原生 iOS UI。
-
控件列表: 深入探索 JSBox 提供的各种 UI 控件及其特定用法。
-
数据类型与内置函数: 理解 JavaScript 与 Native 数据转换,并掌握常用辅助函数。
-
Promise 与高级特性: 学习异步编程的最佳实践,以及更多强大的扩展功能。
-
包管理: 了解如何组织大型、模块化、可维护的 JSBox 项目。
-
Objective-C Runtime: 探索 JSBox 的终极武器,直接与 iOS 原生底层交互。
最后,我们将通过一个完整的“提醒应用小程序”案例来综合运用所有知识。
阶段一:快速开始 (Quick Start)
本阶段是 JSBox 学习的起点,旨在让你对 JSBox 的基本哲学、代码风格和运行方式有一个初步的认识。
核心概念
-
JavaScript 驱动: JSBox 脚本基于 JavaScript (支持 ES6 标准语法)。
-
API 风格: 所有 JSBox API 都以 ui, $http)。
-
轻量化与移动优先: API 设计简洁,适合移动端编写。
-
沙盒环境: 每个脚本在独立沙盒中运行。
-
运行方式: App 内编写、URL Scheme 安装、VSCode 同步、AirDrop 传输等。
常用 API 示例
- 弹出简单提示
$ui.alert("Hello, JSBox Learner!");
$ui.alert({
title: "欢迎学习",
message: "这是你的第一个 JSBox 提示框。",
actions: ["好的"]
});
- 打印日志到控制台
console.log("这是一个普通的日志信息。");
console.info("应用启动成功。");
console.warn("注意:某个参数可能为空。");
console.error("发生了一个错误!");
- 获取剪贴板内容并预览
const clipboardText = $clipboard.text;
if (clipboardText) {
$ui.preview({ title: "剪贴板内容", text: clipboardText });
$ui.toast("已获取剪贴板文本并预览。");
} else {
$ui.toast("剪贴板中没有文本内容。");
}
const clipboardItems = $clipboard.items;
if (clipboardItems && clipboardItems.length > 0) {
$ui.preview({
title: "剪贴板所有项目",
text: JSON.stringify(clipboardItems, null, 2)
});
}
阶段二:基础接口 (Foundation APIs)
涵盖模块:device, cache, file, system, l10n。
常用 API 示例
- $app
console.log("JSBox 应用信息:", $app.info);
console.log("当前脚本运行环境:", $app.env);
$app.idleTimerDisabled = true;
$ui.toast("屏幕将保持常亮。");
$app.openURL("https://www.apple.com");
$app.openURL("weixin://");
- $device
const nt = $device.networkType;
const mapNetworkType = { 0: "无网络", 1: "Wi‑Fi", 2: "蜂窝" };
console.log("当前网络类型:", mapNetworkType[nt] ?? "未知");
const deviceInfo = $device.info;
$ui.alert({
title: "设备信息",
message: `
型号: ${deviceInfo.model}
系统版本: ${deviceInfo.version}
屏幕宽度: ${deviceInfo.screen.width} px
屏幕高度: ${deviceInfo.screen.height} px
电池电量: ${(deviceInfo.battery.level * 100).toFixed(0)}%
网络类型: ${(function () {
const nt = $device.networkType;
const map = { 0: "无网络", 1: "Wi‑Fi", 2: "蜂窝" };
return map[nt] ?? "未知";
})()}
当前是否深色模式: ${$device.isDarkMode ? "是" : "否"}
`
});
$device.taptic(0);
$ui.toast("设备轻微振动了一下。");
- $http
$ui.toast("将发起网络请求获取今日诗词...");
$ui.loading(true);
$http.get({
url: "https://v1.hitokoto.cn/?c=a&c=b&c=c&c=d&c=e&c=f&c=g&c=h&c=i&c=j&c=k&encode=json",
handler: function (resp) {
$ui.loading(false);
if (resp.error) {
$ui.alert("请求失败: " + resp.error.localizedDescription);
console.error("HTTP 请求错误:", resp.error);
return;
}
const data = resp.data;
if (data && data.hitokoto) {
$ui.alert({ title: "一言", message: `${data.hitokoto}\n---- ${data.from}` });
console.log("今日一言数据:", data);
} else {
$ui.alert("未获取到有效数据。");
}
}
});
// 下载图片并分享(可选)
// const imageUrl = $clipboard.link || "https://example.com/example.png";
// if (imageUrl) {
// $ui.toast("将下载图片并尝试分享...");
// $ui.loading(true);
// $http.download({
// url: imageUrl,
// showsProgress: true,
// message: "下载图片中...",
// handler: function (resp) {
// $ui.loading(false);
// if (resp.error) return $ui.alert("图片下载失败: " + resp.error.localizedDescription);
// if (resp.data) {
// $share.sheet(resp.data);
// $ui.toast("图片下载完成并已调起分享。");
// }
// }
// });
// }
- $file(同步 API)
const filename = "my_temp_log.txt";
const foldername = "my_data_folder";
const writeSuccess = $file.write({
data: $data({ string: "日志内容:" + new Date().toLocaleString() + "\n" }),
path: filename
});
if (writeSuccess) {
$ui.toast("文件写入成功: " + filename);
console.log("绝对路径:", $file.absolutePath(filename));
} else {
$ui.alert("文件写入失败!");
}
if ($file.exists(filename)) {
const fileData = $file.read(filename);
if (fileData && fileData.string) {
$ui.alert({ title: "文件内容", message: fileData.string });
}
}
if (!$file.exists(foldername)) {
const ok = $file.mkdir(foldername);
if (ok) $ui.toast("文件夹创建成功: " + foldername);
}
console.log("当前脚本沙盒内容:", $file.list("./"));
// $file.delete(filename);
- $cache(同步 API)
$cache.set("appSettings", { username: "JSBoxUser", theme: "dark", fontSize: 16 });
$ui.toast("设置已缓存!");
console.log("从缓存中读取:", $cache.get("appSettings"));
// $cache.clear();
- $clipboard
$ui.alert({ title: "当前剪贴板文本", message: $clipboard.text || "剪贴板为空" });
$clipboard.text = "这段文字是从 JSBox 设置的!";
$ui.toast("剪贴板文本已更新。");
console.log("剪贴板中的链接:", $clipboard.link);
console.log("剪贴板中的电话号码:", $clipboard.phoneNumber);
- $thread 与延时
$ui.loading(true);
$ui.toast("后台任务进行中...");
$delay(0.1, () => {
$thread.background(() => {
for (let i = 0; i < 10_000_000; i++) {}
$thread.main(() => {
$ui.loading(false);
$ui.toast("后台任务完成!");
});
});
});
$delay(2, () => $ui.toast("2 秒后执行的任务。"));
(async () => {
$ui.toast("等待 3 秒...");
await $wait(3);
$ui.alert("等待结束,弹出 Alert。");
})();
- $system(注意权限与系统限制)
// $system.brightness = 0.5;
// $system.volume = 0.8; // 可能受 iOS 限制而无效
// 拨打电话
// $system.call("10086");
// 短信与邮件(推荐)
/*
await $message.sms({ recipients: ["10086"], body: "你好,我想查询话费。" });
await $message.mail({ to: ["log.e@qq.com"], subject: "测试邮件", body: "这是来自 JSBox 的测试邮件。" });
*/
// mailto
// $app.openURL("mailto:xxx@example.com?subject=测试&body=来自JSBox");
- $keychain(同步 API)
const KEY = "mySecretPassword";
const DOMAIN = "com.myapp.domain";
const setOk = $keychain.set(KEY, "your_secure_password_123", DOMAIN);
if (setOk) $ui.toast("密码已安全存储。");
const pwd = $keychain.get(KEY, DOMAIN);
if (pwd) $ui.alert({ title: "获取到密码", message: pwd });
// $keychain.remove(KEY, DOMAIN);
- app.strings)
$app.strings = {
"en": { "APP_NAME": "My Applet", "GREETING": "Hello, how are you?", "OK": "OK" },
"zh-Hans": { "APP_NAME": "我的小程序", "GREETING": "你好,你好吗?", "OK": "好的" }
};
$ui.alert({
title: $l10n("APP_NAME"),
message: $l10n("GREETING"),
actions: [$l10n("OK")]
});
阶段三:构建界面 (Build UI)
核心概念
-
UI 树结构,ui.push。
-
props/layout/events。
-
$(id) 获取视图实例。
-
Dark Mode 适配:$color({ light, dark })。
-
修改当前页面背景:ui.window.bgcolor。
示例
- 基本页面
$ui.render({
props: {
title: "我的第一个 UI 页面",
navBarHidden: false,
bgcolor: $color("systemBackground"),
theme: "auto"
},
views: [
{
type: "label",
props: {
text: "Hello, UI World!",
textColor: $color("label"),
font: $font("bold", 28),
align: $align.center
},
layout: (make, view) => {
make.centerX.equalTo(view.super);
make.centerY.equalTo(view.super).offset(-50);
make.width.equalTo(view.super).multipliedBy(0.8);
make.height.equalTo(50);
},
events: {
tapped: () => $ui.toast("你点击了标签!")
}
}
]
});
- 页面跳转
$ui.render({
props: { title: "主页面" },
views: [
{
type: "button",
props: { title: "进入详情" },
layout: $layout.center,
events: {
tapped: () => {
$ui.push({
props: { title: "详情页面", bgcolor: $color("systemBackground") },
views: [{ type: "label", props: { text: "这是详情内容", align: $align.center }, layout: $layout.fill }]
});
}
}
}
]
});
- 动态修改视图
$ui.render({
views: [
{
type: "label",
props: {
id: "myDynamicLabel",
text: "点击按钮改变我!",
textColor: $color("label"),
font: $font(20),
align: $align.center
},
layout: (make) => {
make.centerX.equalTo(make.super);
make.top.inset(100);
make.width.equalTo(250);
make.height.equalTo(40);
}
},
{
type: "button",
props: { title: "改变文本" },
layout: (make) => {
make.centerX.equalTo(make.super);
make.top.equalTo($("myDynamicLabel").bottom).offset(30);
make.size.equalTo($size(120, 40));
},
events: {
tapped: () => {
const label = $("myDynamicLabel");
label.text = "文本已改变!" + new Date().getSeconds();
label.textColor = $color($rgb(Math.random() * 255, Math.random() * 255, Math.random() * 255));
$ui.toast("文本已更新!");
}
}
}
]
});
- 复杂布局
$ui.render({
views: [
{
type: "view",
props: { id: "redView", bgcolor: $color("red"), cornerRadius: 10 },
layout: (make) => {
make.left.top.inset(20);
make.width.equalTo(100);
make.height.equalTo(100);
}
},
{
type: "view",
props: { id: "blueView", bgcolor: $color("blue"), cornerRadius: 10 },
layout: (make) => {
make.top.equalTo($("redView").top);
make.left.equalTo($("redView").right).offset(10);
make.size.equalTo($("redView"));
}
},
{
type: "view",
props: { bgcolor: $color("green"), cornerRadius: 10 },
layout: (make, view) => {
make.top.equalTo($("blueView").bottom).offset(10);
make.right.inset(20);
make.width.equalTo(view.super).multipliedBy(0.5).offset(-20);
make.height.equalTo(50);
}
}
]
});
- 手势与事件
$ui.render({
views: [
{
type: "view",
props: { bgcolor: $color("purple"), userInteractionEnabled: true },
layout: $layout.center,
events: {
longPressed: (info) => $ui.toast(`长按!(${info.location.x.toFixed(0)}, ${info.location.y.toFixed(0)})`),
doubleTapped: (sender) => {
sender.alpha = sender.alpha === 1 ? 0.5 : 1;
$ui.toast("双击:透明度切换。");
},
touchesBegan: (s, loc, locs) => console.log("触摸开始,触点数:", locs.length),
touchesEnded: () => console.log("触摸结束。")
}
}
]
});
- 菜单(上下文与下拉)
$ui.render({
props: {
title: "菜单示例",
navButtons: [
{
symbol: "ellipsis.circle",
tintColor: $color("tintColor"),
menu: {
title: "更多操作",
pullDown: true,
items: [
{ title: "刷新", symbol: "arrow.clockwise", handler: () => $ui.toast("刷新中...") },
{ title: "分享", symbol: "square.and.arrow.up", handler: () => $share.sheet("分享内容") },
{
title: "高级",
items: [
{ title: "设置", symbol: "gearshape", handler: () => $ui.toast("打开设置") },
{ title: "关于", symbol: "info.circle", handler: () => $ui.toast("关于脚本") }
]
},
{ title: "删除数据", symbol: "trash", destructive: true, handler: () => $ui.alert("确认删除?") }
]
}
}
]
},
views: [
{
type: "label",
props: {
text: "长按我查看上下文菜单\n或点击导航栏按钮",
align: $align.center,
lines: 0,
textColor: $color("label"),
menu: {
title: "上下文操作",
items: [
{ title: "复制", symbol: "doc.on.doc", handler: () => { $clipboard.text = "复制的内容"; $ui.toast("已复制"); } },
{ title: "分享", symbol: "square.and.arrow.up", handler: () => $share.sheet("通过上下文菜单分享的内容") }
]
}
},
layout: $layout.center
}
]
});
阶段四:控件列表 (Components)
- label
{
type: "label",
props: {
text: "这是一个标签",
font: $font("ChalkboardSE-Bold", 24),
textColor: $color("red"),
shadowColor: $color("systemGray"),
align: $align.left,
lines: 2
},
layout: $layout.fill
}
- button
{
type: "button",
props: {
title: "点击我",
titleColor: $color("white"),
bgcolor: $color("blue"),
icon: $icon("007", $color("yellow"), $size(20, 20)),
symbol: "folder.fill",
imageEdgeInsets: $insets(0, 0, 0, 10)
},
events: { tapped: () => $ui.toast("按钮被点击了!") },
layout: $layout.center
}
- input
{
type: "input",
props: {
placeholder: "请输入文本",
type: $kbType.default,
darkKeyboard: true,
text: "初始文本",
secure: true
},
events: {
changed: (sender) => console.log("输入变化:", sender.text),
returned: (sender) => $ui.alert(`你输入了: ${sender.text}`)
},
layout: $layout.fill
}
- text
{
type: "text",
props: {
placeholder: "请输入多行文本...",
editable: true,
selectable: true,
insets: $insets(10, 10, 10, 10),
styledText: "**粗体** *斜体* [链接](https://jsbox.app)",
font: $font(16)
},
events: {
didChange: (sender) => console.log("文本变化:", sender.text),
didChangeSelection: (sender) => console.log("选中区域变化:", sender.selectedRange)
},
layout: $layout.fill
}
- list
{
type: "list",
props: {
id: "list",
rowHeight: 70,
autoRowHeight: false,
separatorHidden: false,
separatorColor: $color("separatorColor"),
reorder: true,
data: [
{
title: "水果",
rows: [
{ label: { text: "苹果" }, icon: { symbol: "apple.logo" } },
{ label: { text: "香蕉" }, icon: { symbol: "leaf.fill" } }
]
},
{
title: "动物",
rows: [
{ label: { text: "猫" }, icon: { symbol: "pawprint.fill" } },
{ label: { text: "狗" }, icon: { symbol: "hare.fill" } }
]
}
],
template: {
views: [
{
type: "image",
props: { id: "icon", tintColor: $color("tintColor") },
layout: (make) => { make.left.inset(15); make.centerY.equalTo(make.super); make.size.equalTo($size(30, 30)); }
},
{
type: "label",
props: { id: "label" },
layout: (make) => { make.left.equalTo($("icon").right).offset(10); make.right.inset(15); make.centerY.equalTo(make.super); }
}
]
},
actions: [
{
title: "删除",
color: $color("red"),
handler: (sender, indexPath) => sender.delete(indexPath)
},
{
title: "编辑",
color: $color("blue"),
handler: (sender, indexPath) => $ui.toast(`编辑 ${sender.data[indexPath.section].rows[indexPath.row].label.text}`)
}
]
},
events: {
didSelect: (sender, indexPath, data) => $ui.toast(`选择了: ${data.label.text}`),
reorderMoved: (fromPath, toPath) => console.log(`从 ${fromPath.section}-${fromPath.row} 到 ${toPath.section}-${toPath.row}`),
pulled: (sender) => {
$ui.toast("加载更多数据...");
$ui.loading(true);
$delay(2, () => {
const currentData = $("list").data;
const newData = Array(5).fill(0).map((_, i) => ({ label: { text: `新项目 ${currentData[0].rows.length + i + 1}` } }));
currentData[0].rows.push(...newData);
$("list").data = currentData;
sender.endRefreshing();
$ui.loading(false);
});
},
didReachBottom: (sender) => {
$ui.toast("加载更多数据...");
$ui.loading(true);
$delay(1.5, () => {
const currentData = $("list").data;
const newItems = Array(5).fill(0).map((_, i) => ({ label: { text: `更多项 ${currentData[0].rows.length + i + 1}` }, icon: { symbol: "cloud.fill" } }));
currentData[0].rows.push(...newItems);
$("list").data = currentData;
sender.endFetchingMore();
$ui.loading(false);
});
}
},
layout: $layout.fill
}
- matrix
{
type: "matrix",
props: {
columns: 3,
itemHeight: 120,
spacing: 5,
data: Array(10).fill(0).map((_, i) => ({
label: { text: `Item ${i + 1}` },
image: { symbol: `star.fill` }
})),
template: {
views: [
{
type: "image",
props: { id: "image", tintColor: $color("systemYellow") },
layout: (make) => { make.centerX.equalTo(make.super); make.top.inset(10); make.size.equalTo($size(60, 60)); }
},
{
type: "label",
props: { id: "label", align: $align.center, font: $font(14), textColor: $color("label") },
layout: (make) => { make.centerX.equalTo(make.super); make.top.equalTo($("image").bottom).offset(5); make.width.equalTo(make.super); make.height.equalTo(20); }
}
]
}
},
events: { didSelect: (sender, indexPath, data) => $ui.toast(`点击了: ${data.label.text}`) },
layout: $layout.fill
}
- web
{
type: "web",
props: {
url: "https://www.apple.com/cn/",
showsProgress: true,
toolbar: true,
scrollEnabled: true,
script: function () {
const heading = document.querySelector('h1');
if (heading) $notify("webContentLoaded", { text: heading.innerText });
}
},
events: {
didFinish: () => $ui.toast("网页加载完成!"),
didFail: (sender, navigation, error) => $ui.alert("网页加载失败: " + error.localizedDescription),
decideNavigation: (sender, action) => {
// 若取不到 requestURL,可尝试 action.URL 或 action.url,按当前文档为准
if (action.requestURL && action.requestURL.startsWith("https://apple.com/cn/iphone/")) return false;
return true;
},
webContentLoaded: (object) => $ui.alert(`来自网页的消息:${object.text}`)
},
layout: $layout.fill
}
- picker
$ui.render({
views: [
{
type: "button",
props: { title: "选择日期" },
layout: (make) => { make.centerX.equalTo(make.super); make.top.inset(50); make.size.equalTo($size(120, 40)); },
events: {
tapped: async () => {
const selectedDate = await $picker.date({ mode: 0, date: new Date() });
if (selectedDate) $ui.alert(`你选择了日期: ${selectedDate.toLocaleString()}`);
else $ui.toast("取消日期选择。");
}
}
},
{
type: "button",
props: { title: "选择颜色" },
layout: (make) => { make.centerX.equalTo(make.super); make.top.equalTo($("button").bottom).offset(20); make.size.equalTo($size(120, 40)); },
events: {
tapped: async () => {
const selectedColor = await $picker.color({ color: $color("red") });
if (selectedColor) {
$ui.alert(`你选择了颜色: ${selectedColor.hexCode}`);
$ui.controller.view.bgcolor = selectedColor;
} else {
$ui.toast("取消颜色选择。");
}
}
}
},
{
type: "button",
props: { title: "通用选择器" },
layout: (make) => { make.centerX.equalTo(make.super); make.top.equalTo($("button").bottom).offset(20); make.size.equalTo($size(150, 40)); },
events: {
tapped: async () => {
const result = await $picker.data({ items: [["选项 A1", "选项 A2", "选项 A3"], ["选项 B1", "选项 B2"]] });
if (result) {
$ui.alert(`你选择了: ${result.data[0]} 和 ${result.data[1]} (索引: ${result.selectedRows[0]}, ${result.selectedRows[1]})`);
} else {
$ui.toast("取消通用选择。");
}
}
}
}
]
});
阶段五:数据类型与内置函数 (Data Types & Built-in Functions)
核心概念
-
数据类型构造:size, insets, font, image, indexPath 等。
-
动态适配:image({ light, dark })。
-
全局常量:env, contentMode, mediaType, popoverDirection 等。
-
辅助函数:delay, props, $desc 等。
示例
- 几何与颜色
$ui.render({
props: { title: "几何与颜色", bgcolor: $color("systemBackground"), theme: "auto" },
views: [
{
type: "view",
props: {
frame: $rect(20, 20, 100, 100),
bgcolor: $rgba(255, 0, 0, 0.7),
cornerRadius: 10,
borderWidth: 2,
borderColor: $color({ light: "#0000FF", dark: "#00FFFF" })
}
},
{
type: "label",
props: { text: "动态颜色文本", textColor: $color("label"), font: $font("bold", 20) },
layout: (make) => { make.centerX.equalTo(make.super); make.top.inset(150); }
}
]
});
- image/$icon
const base64String = "SGVsbG8sIFdvcmxkIQ==";
const textData = $data({ base64: base64String, encoding: 4 });
console.log("Base64 解码文本:", textData.string);
const transparentGifBase64 = "R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==";
$ui.render({
views: [
{
type: "image",
props: {
image: $image("data:image/gif;base64," + transparentGifBase64),
symbol: "person.circle.fill",
tintColor: $color("systemGreen"),
contentMode: $contentMode.scaleAspectFit
},
layout: (make) => { make.centerX.equalTo(make.super); make.top.inset(50); make.size.equalTo($size(80, 80)); }
},
{
type: "label",
props: { icon: $icon("005", $color("systemGreen"), $size(24, 24)), text: " JSBox 内置图标", font: $font(18), textColor: $color("label") },
layout: (make) => { make.centerX.equalTo(make.super); make.top.equalTo($("image").bottom).offset(20); }
}
]
});
- range
const p = $indexPath(1, 5);
console.log(`区: ${p.section}, 行: ${p.row}`);
const r = $range(10, 5);
console.log(`范围起始: ${r.location}, 长度: ${r.length}`);
- 延时
$ui.toast("将在 1 秒后消失...");
$delay(1, () => $ui.alert("提示已消失。"));
(async () => {
$ui.toast("等待 2 秒...");
await $wait(2);
$ui.alert("等待结束,弹出 Alert。");
})();
阶段六:Promise 与高级特性 (Promise & Advanced Features)
核心概念
-
Promise/async-await。
-
JSBox API 的 required/optional 两种 Promise 模式(optional 需 async: true)。
-
扩展 API:qrcode, browser, share, $push。
-
原生 SDK:reminder, location, message, $safari。
示例
- 异步链
$ui.render({
props: { title: "Async/Await 演示", theme: "auto" },
views: [
{
type: "button",
props: { title: "开始异步链" },
layout: $layout.center,
events: {
tapped: async () => {
$ui.loading(true);
$ui.toast("步骤 1/3: 请求数据...");
try {
const httpResp = await $http.get("https://fanyi.youdao.com/openapi.do?keyfrom=JSBox_Test&key=139365261&type=data&doctype=json&version=1.1&q=hello");
$ui.loading(false);
if (httpResp.error) throw new Error("网络请求失败: " + httpResp.error.localizedDescription);
const translation = httpResp.data.translation[0];
$ui.toast("步骤 1 完成:翻译成功!");
$ui.loading(true);
$ui.toast("步骤 2/3: 请输入文本,结果将 Base64 编码...");
const inputText = await $input.text({ placeholder: "请输入一个短语进行 Base64 编码" });
$ui.loading(false);
if (!inputText) return $ui.toast("取消输入,停止操作。");
const encodedText = $text.base64Encode(inputText);
$ui.toast("步骤 2 完成:文本已编码!");
$ui.loading(true);
$ui.toast("步骤 3/3: 请从相册选取一张图片...");
const photoResp = await $photo.pick({ mediaTypes: [$mediaType.image], async: true });
$ui.loading(false);
if (photoResp && photoResp.image) {
$ui.alert({
title: "所有步骤完成!",
message: `
原文: ${inputText} -> 翻译: ${translation}
编码后: ${encodedText}
图片选取成功!
`
});
} else {
$ui.toast("步骤 3 取消或未选取图片。");
}
} catch (e) {
$ui.loading(false);
$ui.alert("操作失败: " + e.message);
console.error("异步链错误:", e);
}
}
}
}
]
});
- $text
console.log("UUID:", $text.uuid);
const originalUrl = "https://www.example.com?param=中文内容&key=value";
console.log("URL 编码:", $text.URLEncode(originalUrl));
console.log("URL 解码:", $text.URLDecode($text.URLEncode(originalUrl)));
console.log("MD5:", $text.MD5("JSBox"));
console.log("SHA1:", $text.SHA1("JSBox"));
console.log("SHA256:", $text.SHA256("JSBox"));
const markdownText = "# 标题\n**粗体文本** *斜体文本*";
const htmlText = "<h1>标题</h1><b>粗体文本</b><i>斜体文本</i>";
const convertedHtml = $text.markdownToHtml(markdownText);
console.log("Markdown 转 HTML:", convertedHtml);
(async () => {
const markdown = await $text.htmlToMarkdown({ html: htmlText });
console.log("HTML 转 Markdown:", markdown);
})();
- $qrcode
$ui.render({
props: { title: "二维码示例" },
views: [
{
type: "image",
props: { id: "qrcodeImage", image: $qrcode.encode("https://jsbox.app"), contentMode: $contentMode.scaleAspectFit },
layout: (make) => { make.center.equalTo(make.super); make.size.equalTo($size(150, 150)); }
},
{
type: "button",
props: { title: "扫描二维码" },
layout: (make) => { make.centerX.equalTo(make.super); make.top.equalTo($("qrcodeImage").bottom).offset(30); make.size.equalTo($size(120, 40)); },
events: {
tapped: async () => {
const scanResult = await $qrcode.scan();
if (scanResult) $ui.alert(`扫描结果: ${scanResult}`);
else $ui.toast("取消扫描。");
}
}
}
]
});
- $archiver
$file.write({ data: $data({ string: "Hello world for zip test!" }), path: "test.txt" });
$ui.loading(true);
$ui.toast("文件压缩中...");
$archiver.zip({
paths: ["test.txt"],
dest: "myarchive.zip",
handler: async (success) => {
$ui.loading(false);
if (success) {
$ui.toast("文件压缩成功!");
$ui.loading(true);
$ui.toast("文件解压中...");
const unzipSuccess = await $archiver.unzip({ path: "myarchive.zip", dest: "unzipped_folder" });
$ui.loading(false);
if (unzipSuccess) {
$ui.toast("文件解压成功!");
console.log("解压后的文件:", $file.list("unzipped_folder"));
} else {
$ui.alert("文件解压失败!");
}
} else {
$ui.alert("文件压缩失败!");
}
}
});
- Native SDK(calendar)
$ui.render({
props: { title: "Native SDK 示例" },
views: [
{
type: "button",
props: { title: "选取照片" },
layout: (make) => { make.centerX.equalTo(make.super); make.top.inset(50); make.size.equalTo($size(120, 40)); },
events: {
tapped: async () => {
$ui.loading(true);
try {
const resp = await $photo.pick({ mediaTypes: [$mediaType.image], async: true });
$ui.loading(false);
if (resp && resp.image) $ui.alert("选取照片成功!");
else $ui.toast("未选取照片或取消。");
} catch (e) {
$ui.loading(false);
$ui.alert("选取照片失败: " + e.message);
}
}
}
},
{
type: "button",
props: { title: "创建日历事件" },
layout: (make) => { make.centerX.equalTo(make.super); make.top.equalTo($("button").bottom).offset(20); make.size.equalTo($size(150, 40)); },
events: {
tapped: async () => {
$ui.loading(true);
try {
const now = new Date();
const tomorrow = new Date(now.getTime() + 24 * 3600 * 1000);
const resp = await $calendar.create({
title: "JSBox 学习任务",
startDate: now,
endDate: tomorrow,
notes: "完成 JSBox 学习材料的阅读和实践。",
url: "https://jsbox.app",
async: true
});
$ui.loading(false);
if (resp && resp.status) $ui.toast("日历事件创建成功!");
else $ui.alert("日历事件创建失败或取消。");
} catch (e) {
$ui.loading(false);
$ui.alert("创建日历事件失败: " + e.message);
}
}
}
}
]
});
阶段七:包管理 (Package Management)
项目结构示例
MyPackageScript/
├── main.js
├── config.json
├── strings/
│ ├── en.strings
│ └── zh-Hans.strings
├── scripts/
│ └── data_manager.js
└── assets/
└── icon.png
- main.js
// main.js
const dataManager = require('./scripts/data_manager');
$app.strings = {
"en": { "APP_TITLE": "My Applet", "WELCOME_MSG": "Welcome to Applet!", "LOAD_DATA": "Load Data", "SAVE_DATA": "Save Data", "COUNT": "Count: ", "OK": "OK" },
"zh-Hans": { "APP_TITLE": "我的小程序", "WELCOME_MSG": "欢迎使用小程序!", "LOAD_DATA": "加载数据", "SAVE_DATA": "保存数据", "COUNT": "数量:", "OK": "好的" }
};
$ui.render({
props: { title: $l10n("APP_TITLE"), bgcolor: $color("systemBackground"), theme: "auto" },
views: [
{
type: "image",
props: { id: "image", src: "assets/icon.png", contentMode: $contentMode.scaleAspectFit },
layout: (make) => { make.centerX.equalTo(make.super); make.top.inset(50); make.size.equalTo($size(80, 80)); }
},
{
type: "label",
props: { id: "countLabel", text: $l10n("COUNT") + "0", align: $align.center, font: $font(20), textColor: $color("label") },
layout: (make) => { make.centerX.equalTo(make.super); make.top.equalTo($("image").bottom).offset(30); make.width.equalTo(250); make.height.equalTo(30); }
},
{
type: "button",
props: { title: $l10n("LOAD_DATA") },
layout: (make) => { make.centerX.equalTo(make.super); make.top.equalTo($("countLabel").bottom).offset(30); make.size.equalTo($size(150, 40)); },
events: {
tapped: async () => {
$ui.loading(true);
const data = await dataManager.loadData();
$ui.loading(false);
$("countLabel").text = $l10n("COUNT") + data.length;
$ui.toast("数据加载完成!");
}
}
},
{
type: "button",
props: { title: $l10n("SAVE_DATA") },
layout: (make) => { make.centerX.equalTo(make.super); make.top.equalTo($("button").bottom).offset(20); make.size.equalTo($size(150, 40)); },
events: {
tapped: async () => {
$ui.loading(true);
const currentData = await dataManager.loadData();
currentData.push({ timestamp: Date.now() });
const success = await dataManager.saveData(currentData);
$ui.loading(false);
if (success) {
$("countLabel").text = $l10n("COUNT") + currentData.length;
$ui.toast("数据保存成功!");
} else {
$ui.alert("数据保存失败!");
}
}
}
}
]
});
(async () => {
const initialData = await dataManager.loadData();
$("countLabel").text = $l10n("COUNT") + initialData.length;
})();
- scripts/data_manager.js
// scripts/data_manager.js
const DATA_FILE = "my_app_data.json";
async function loadData() {
if (!$file.exists(DATA_FILE)) return [];
try {
const data = $file.read(DATA_FILE);
if (data && data.string) return JSON.parse(data.string);
} catch (e) {
console.error("Error parsing data file:", e);
$ui.alert("加载数据失败!");
}
return [];
}
async function saveData(data) {
try {
const dataString = JSON.stringify(data, null, 2);
const success = $file.write({ data: $data({ string: dataString, encoding: 4 }), path: DATA_FILE });
return success;
} catch (e) {
console.error("Error saving data file:", e);
$ui.alert("保存数据失败!");
return false;
}
}
module.exports = { loadData, saveData };
- strings/en.strings
"APP_TITLE" = "My Package App";
"WELCOME_MSG" = "Welcome!";
"LOAD_DATA" = "Load Data";
"SAVE_DATA" = "Save Data";
"COUNT" = "Count: ";
"OK" = "OK";
- strings/zh-Hans.strings
"APP_TITLE" = "我的打包应用";
"WELCOME_MSG" = "欢迎!";
"LOAD_DATA" = "加载数据";
"SAVE_DATA" = "保存数据";
"COUNT" = "数量:";
"OK" = "好的";
阶段八:Objective-C Runtime (Runtime)
核心概念
-
method 调用方法。
-
jsValue()/ocValue() 在 JS 与 Native 之间转换。
-
delegate 动态定义类与委托。
-
$block 封装回调。
示例
- 调用原生方法
const UIColor = $objc("UIColor");
const UIApplication = $objc("UIApplication");
const NSURL = $objc("NSURL");
const app = UIApplication.$sharedApplication();
const url = NSURL.$URLWithString("https://www.google.com");
if (app.$canOpenURL(url)) {
app.$openURL(url);
$ui.toast("已尝试打开 Google");
} else {
$ui.alert("无法打开 Google");
}
// const UIScreen = $objc("UIScreen");
// const screen = UIScreen.$mainScreen();
// const currentBrightness = screen.$brightness();
// $ui.alert(`当前屏幕亮度: ${(currentBrightness * 100).toFixed(0)}%`);
// screen.$setBrightness(0.5);
- 原生 Alert + Block
$ui.render({
props: { title: "原生 Alert 演示" },
views: [
{
type: "button",
props: { title: "显示原生 Alert" },
layout: $layout.center,
events: {
tapped: () => {
const alertController = $objc("UIAlertController").$alertControllerWithTitle_message_preferredStyle_(
"原生 Alert 标题",
"这个 Alert 是通过 Objective-C Runtime API 创建的!",
1
);
const defaultAction = $objc("UIAlertAction").$actionWithTitle_style_handler_(
"好的",
0,
$block("void, id", () => $ui.toast("你点击了 '好的'。"))
);
const cancelAction = $objc("UIAlertAction").$actionWithTitle_style_handler_(
"取消",
1,
$block("void, id", () => $ui.toast("你点击了 '取消'。"))
);
alertController.$addAction(defaultAction);
alertController.$addAction(cancelAction);
const currentVC = $ui.controller.ocValue();
currentVC.$presentViewController_animated_completion_(alertController, true, null);
}
}
}
]
});
- 混合视图(原生 UILabel)
$ui.render({
props: { title: "原生标签混合显示", bgcolor: $color("systemBackground"), theme: "auto" },
views: [
{
type: "label",
props: { id: "jsLabel", text: "这是 JSBox UI 定义的标签", align: $align.center, font: $font("bold", 18), textColor: $color("label") },
layout: (make) => { make.centerX.equalTo(make.super); make.top.inset(50); make.width.equalTo(280); make.height.equalTo(30); }
},
{
type: "button",
props: { id: "runtimeTargetButton", title: "点击查看 Runtime 效果", bgcolor: $color("systemOrange"), titleColor: $color("white"), cornerRadius: 8 },
layout: (make) => { make.centerX.equalTo(make.super); make.top.equalTo($("jsLabel").bottom).offset(30); make.size.equalTo($size(200, 45)); }
}
]
});
$thread.main(() => {
const rootView = $ui.controller.view.ocValue();
const nativeLabel = $objc("UILabel").$new();
nativeLabel.$setText("这是由 Runtime 创建的原生标签");
nativeLabel.$setTextColor($objc("UIColor").$systemBlueColor());
nativeLabel.$setTextAlignment(1);
nativeLabel.$setFont($objc("UIFont").$systemFontOfSize(18));
const nativeLabelJSView = nativeLabel.jsValue();
nativeLabelJSView.layout = (make) => {
make.centerX.equalTo(make.super);
make.top.equalTo($("runtimeTargetButton").bottom).offset(30);
make.width.equalTo(300);
make.height.equalTo(30);
};
rootView.$addSubview(nativeLabel);
});
综合应用案例:提醒应用小程序 (增强版)
结构
MyReminderApp/
├── main.js
├── config.json
├── strings/
│ ├── en.strings
│ └── zh-Hans.strings
├── scripts/
│ ├── reminder_model.js
│ ├── reminder_view.js
│ └── util.js
└── assets/
└── icon.png
- config.json
{
"info": {
"name": "MyReminderApp",
"version": "1.0.0",
"author": "YourName",
"icon": "icon.png",
"category": "工具"
},
"settings": {
"theme": "auto",
"minSDKVer": "2.12.0"
}
}
- strings/en.strings
"APP_TITLE" = "My Reminders";
"ADD_REMINDER" = "Add Reminder";
"EDIT_REMINDER" = "Edit Reminder";
"NEW_REMINDER" = "New Reminder";
"REMINDER_TEXT_PLACEHOLDER" = "Reminder Text";
"REMINDER_DATE" = "Reminder Date";
"SAVE" = "Save";
"DELETE" = "Delete";
"CANCEL" = "Cancel";
"REMINDER_SAVED_SUCCESS" = "Reminder saved!";
"REMINDER_DELETED_SUCCESS" = "Reminder deleted!";
"REMINDER_FETCH_FAILED" = "Failed to load reminders.";
"NO_REMINDERS" = "No reminders yet. Add one!";
"OK" = "OK";
"CONFIRM_DELETE_TITLE" = "Confirm Delete?";
"CONFIRM_DELETE_MESSAGE" = "Are you sure you want to delete this reminder?";
"NOTIFICATION_PERMISSION_DENIED" = "Notification permission denied. Cannot schedule reminder.";
"CALENDAR_PERMISSION_DENIED" = "Calendar permission denied. Cannot access calendar.";
"REMINDER_PERMISSION_DENIED" = "Reminders permission denied. Cannot access reminders.";
- strings/zh-Hans.strings
"APP_TITLE" = "我的提醒";
"ADD_REMINDER" = "添加提醒";
"EDIT_REMINDER" = "编辑提醒";
"NEW_REMINDER" = "新提醒";
"REMINDER_TEXT_PLACEHOLDER" = "提醒内容";
"REMINDER_DATE" = "提醒日期";
"SAVE" = "保存";
"DELETE" = "删除";
"CANCEL" = "取消";
"REMINDER_SAVED_SUCCESS" = "提醒已保存!";
"REMINDER_DELETED_SUCCESS" = "提醒已删除!";
"REMINDER_FETCH_FAILED" = "加载提醒失败。";
"NO_REMINDERS" = "暂无提醒,点击添加!";
"OK" = "好的";
"CONFIRM_DELETE_TITLE" = "确认删除?";
"CONFIRM_DELETE_MESSAGE" = "你确定要删除这条提醒吗?";
"NOTIFICATION_PERMISSION_DENIED" = "通知权限被拒绝。无法调度提醒。";
"CALENDAR_PERMISSION_DENIED" = "日历权限被拒绝。无法访问日历。";
"REMINDER_PERMISSION_DENIED" = "提醒事项权限被拒绝。无法访问提醒。";
- scripts/util.js(EventKit 权限包装)
// scripts/util.js
function handleError(message, error) {
$ui.loading(false);
$ui.alert(message);
if (error) console.error("Error:", message, error);
else console.error("Error:", message);
}
/**
* 请求日历或提醒事项权限(EventKit)
* @param {"calendar"|"reminder"} entity
* @returns {Promise<boolean>}
*/
async function requestEventKitAccess(entity) {
const EventStoreClass = $objc("EKEventStore");
const store = EventStoreClass.$new();
const type = entity === "reminder" ? 1 : 0; // 0: Event, 1: Reminder
const status = EventStoreClass.$authorizationStatusForEntityType(type);
if (status === 3) return true; // Authorized
if (status === 2) { // Denied
handleError(entity === "calendar" ? $l10n("CALENDAR_PERMISSION_DENIED") : $l10n("REMINDER_PERMISSION_DENIED"));
return false;
}
if (status === 0 || status === 1) {
return await new Promise((resolve) => {
const completion = $block("void, BOOL, id", (granted, error) => {
if (error) {
console.error("EventKit permission error:", error);
resolve(false);
} else {
resolve(!!granted);
}
});
store.$requestAccessToEntityType_completion_(type, completion);
});
}
return false;
}
module.exports = { handleError, requestEventKitAccess };
- scripts/reminder_model.js
// scripts/reminder_model.js
const util = require('./util');
const DATA_FILE = "reminders.json";
let remindersInMemory = [];
async function loadReminders() {
if (!$file.exists(DATA_FILE)) {
remindersInMemory = [];
return [];
}
try {
const data = $file.read(DATA_FILE);
if (data && data.string) {
remindersInMemory = JSON.parse(data.string);
return remindersInMemory;
}
} catch (e) {
console.error("Error parsing reminders.json:", e);
util.handleError($l10n("REMINDER_FETCH_FAILED"), e);
}
remindersInMemory = [];
return [];
}
async function saveReminders() {
try {
const dataString = JSON.stringify(remindersInMemory, null, 2);
const success = $file.write({ data: $data({ string: dataString, encoding: 4 }), path: DATA_FILE });
return success;
} catch (e) {
console.error("Error saving reminders.json:", e);
util.handleError("保存提醒失败!", e);
return false;
}
}
async function addOrUpdateReminder(reminder, isEditing, index) {
if (isEditing) remindersInMemory[index] = reminder;
else remindersInMemory.push(reminder);
return await saveReminders();
}
async function deleteReminder(index) {
remindersInMemory.splice(index, 1);
return await saveReminders();
}
function getRemindersInMemory() {
return remindersInMemory;
}
// 调度通知:无需显式请求通知权限,iOS 首次调度会弹出授权弹窗。
// 若用户拒绝,后续调度可能静默失败,应引导去系统设置开启。
function scheduleNotification(reminder) {
const when = new Date(reminder.date);
if (when > new Date()) {
$push.schedule({
id: reminder.id,
title: $l10n("APP_TITLE"),
body: reminder.text,
date: when,
renew: true
});
console.log("Notification scheduled:", reminder.text);
}
}
function cancelNotification(reminderId) {
$push.cancel({ id: reminderId });
console.log("Notification cancelled:", reminderId);
}
module.exports = {
loadReminders,
saveReminders,
getRemindersInMemory,
addOrUpdateReminder,
deleteReminder,
scheduleNotification,
cancelNotification
};
- scripts/reminder_view.js
// scripts/reminder_view.js
const reminderModel = require('./reminder_model');
const util = require('./util');
async function renderMainView() {
$ui.render({
props: {
title: $l10n("APP_TITLE"),
bgcolor: $color("systemBackground"),
theme: "auto",
navButtons: [
{ symbol: "plus.circle", tintColor: $color("tintColor"), handler: () => pushEditView() }
]
},
views: [
{
type: "list",
props: {
id: "reminderList",
rowHeight: 70,
template: {
views: [
{
type: "label",
props: { id: "reminderText", font: $font(18), lines: 0, textColor: $color("label") },
layout: (make) => { make.left.right.inset(15); make.top.inset(10); make.height.lessThanOrEqualTo(40); }
},
{
type: "label",
props: { id: "reminderDate", font: $font(13), textColor: $color("secondaryLabel") },
layout: (make) => {
make.left.right.inset(15);
make.top.equalTo($("reminderText").bottom).offset(5);
make.height.equalTo(20);
}
},
{
type: "switch",
props: { id: "completedSwitch", onColor: $color("systemGreen") },
layout: (make) => { make.right.inset(15); make.centerY.equalTo(make.super); },
events: {
changed: async (sender) => {
$ui.loading(true);
try {
const index = sender.info.index;
const reminders = reminderModel.getRemindersInMemory();
const reminder = reminders[index];
reminder.completed = sender.on;
const ok = await reminderModel.addOrUpdateReminder(reminder, true, index);
$ui.loading(false);
if (ok) {
$ui.toast(sender.on ? "已完成" : "未完成");
await loadRemindersToList();
} else {
$ui.alert("更新提醒失败。");
sender.on = !sender.on;
}
} catch (e) {
util.handleError("更新提醒状态失败", e);
sender.on = !sender.on;
}
}
}
}
]
},
actions: [
{ title: $l10n("DELETE"), color: $color("red"), handler: (sender, indexPath) => confirmDeleteReminder(indexPath.row) }
]
},
layout: $layout.fill,
events: {
didSelect: (sender, indexPath) => pushEditView(indexPath.row),
pulled: async (sender) => { await loadRemindersToList(); sender.endRefreshing(); }
}
}
]
});
await loadRemindersToList();
// 可选:在进入后台/退出时自动保存(需确认版本支持 $app.listen)
// $app.listen({ pause: async () => await reminderModel.saveReminders(), exit: async () => await reminderModel.saveReminders() });
}
async function loadRemindersToList() {
$ui.loading(true);
await reminderModel.loadReminders();
const reminders = reminderModel.getRemindersInMemory();
$ui.loading(false);
if (reminders.length === 0) {
$("reminderList").data = [{ rows: [{ reminderText: { text: $l10n("NO_REMINDERS") }, completedSwitch: { hidden: true }, reminderDate: { hidden: true } }] }];
$("reminderList").rowHeight = 100;
$("reminderList").selectable = false;
} else {
const rows = reminders.map((r, i) => ({
reminderText: { text: r.text, textColor: r.completed ? $color("secondaryLabel") : $color("label") },
reminderDate: { text: new Date(r.date).toLocaleString(), textColor: r.completed ? $color("secondaryLabel") : $color("secondaryLabel") },
completedSwitch: { on: r.completed, info: { index: i } },
info: { id: r.id, index: i }
}));
$("reminderList").data = [{ rows }];
$("reminderList").rowHeight = 70;
$("reminderList").selectable = true;
}
}
async function pushEditView(index) {
let currentReminder, isEditing = false;
const reminders = reminderModel.getRemindersInMemory();
if (index !== undefined) {
isEditing = true;
currentReminder = { ...reminders[index] };
} else {
currentReminder = { id: Date.now().toString(), text: "", date: new Date().toISOString(), completed: false };
}
$ui.push({
props: { title: isEditing ? $l10n("EDIT_REMINDER") : $l10n("NEW_REMINDER"), bgcolor: $color("systemBackground"), theme: "auto" },
views: [
{
type: "input",
props: { id: "reminderTextInput", placeholder: $l10n("REMINDER_TEXT_PLACEHOLDER"), text: currentReminder.text, font: $font(17), textColor: $color("label"), cornerRadius: 5, bgcolor: $color("secondarySystemBackground") },
layout: (make) => { make.left.right.inset(15); make.top.inset(20); make.height.equalTo(40); }
},
{
type: "label",
props: { id: "reminderDate", text: $l10n("REMINDER_DATE"), textColor: $color("label"), font: $font(15) },
layout: (make) => { make.left.inset(15); make.top.equalTo($("reminderTextInput").bottom).offset(20); }
},
{
type: "date-picker",
props: { id: "reminderDatePicker", mode: 0, date: new Date(currentReminder.date) },
layout: (make) => { make.left.right.inset(15); make.top.equalTo($("reminderDate").bottom).offset(5); make.height.equalTo(180); }
},
{
type: "button",
props: { title: $l10n("SAVE"), bgcolor: $color("systemBlue"), titleColor: $color("white"), cornerRadius: 8 },
layout: (make) => { make.left.right.inset(15); make.top.equalTo($("reminderDatePicker").bottom).offset(30); make.height.equalTo(45); },
events: { tapped: () => saveReminder(currentReminder, isEditing, index) }
},
...(isEditing ? [{
type: "button",
props: { title: $l10n("DELETE"), bgcolor: $color("systemRed"), titleColor: $color("white"), cornerRadius: 8 },
layout: (make) => { make.left.right.inset(15); make.top.equalTo($("button").bottom).offset(15); make.height.equalTo(45); },
events: { tapped: () => confirmDeleteReminder(index, true) }
}] : [])
]
});
}
async function saveReminder(reminder, isEditing, index) {
const textInput = $("reminderTextInput").text;
const datePicker = $("reminderDatePicker").date;
if (!textInput.trim()) return $ui.alert($l10n("REMINDER_TEXT_PLACEHOLDER"));
if (Number.isNaN(datePicker.getTime())) return $ui.alert("无效日期");
reminder.text = textInput.trim();
reminder.date = datePicker.toISOString();
$ui.loading(true);
try {
const ok = await reminderModel.addOrUpdateReminder(reminder, isEditing, index);
if (ok) {
$ui.toast($l10n("REMINDER_SAVED_SUCCESS"));
// 可选:调度本地通知
// reminderModel.scheduleNotification(reminder);
$ui.pop();
await loadRemindersToList();
} else {
util.handleError("保存提醒失败!");
}
} catch (e) {
util.handleError("保存提醒失败!", e);
} finally {
$ui.loading(false);
}
}
async function confirmDeleteReminder(index, fromEditPage = false) {
const confirm = await $ui.alert({
title: $l10n("CONFIRM_DELETE_TITLE"),
message: $l10n("CONFIRM_DELETE_MESSAGE"),
actions: [{ title: $l10n("DELETE"), style: $alertActionType.destructive }, { title: $l10n("CANCEL") }]
});
if (confirm.index !== 0) return;
$ui.loading(true);
try {
const ok = await reminderModel.deleteReminder(index);
if (ok) {
$ui.toast($l10n("REMINDER_DELETED_SUCCESS"));
if (fromEditPage) $ui.pop();
await loadRemindersToList();
} else {
util.handleError("删除提醒失败!");
}
} catch (e) {
util.handleError("删除提醒失败!", e);
} finally {
$ui.loading(false);
}
}
module.exports = { renderMainView };
- main.js
// main.js
const reminderView = require('./scripts/reminder_view');
reminderView.renderMainView();
综合实践
-
打包并安装到 JSBox:将 MyReminderApp 压缩为 zip,分享到 iOS 用 JSBox 打开安装。
-
运行后测试:添加/编辑/删除/完成状态切换/下拉刷新/本地化/暗黑模式适配。
学习总结与展望
-
你已掌握:JSBox 核心 API、原生 UI 构建、模块化组织、异步编程、Runtime 交互。
-
后续优化建议:
-
权限管理:日历/提醒/照片/通知等,统一处理拒绝与异常。
-
错误处理:统一 try-catch 与提示策略。
-
状态管理:内存态 + 关键路径落盘,必要时生命周期补充。
-
体验优化:loading 仅用于阻塞式等待,toast 用于结果提示。
-
进阶功能:搜索过滤、重复提醒、本地通知、数据导入导出、快捷指令与 URL Scheme 集成。
-
调试:充分利用控制台与 Safari 调试工具。
-