界面开发
模块界面是可选的。没有页面也能靠 set_card / push_fields / push_titlebar 在主界面展示数据(见 外壳集成);有页面时,它跑在宿主主界面的 iframe 里,技术栈不限。
页面怎么被加载
- manifest 里声明
"ui": "ui/index.html",这个页面是模块的默认首页; - 页面地址形如
/modules/<模块名>/<相对路径>,与宿主同源,同目录的 js / css / 图片直接相对路径引用(app.js、vendor/vue.js); - 打包成
.jmod后页面照常可用,不需要改任何东西。
多页面注册
ui/ 目录下的所有 HTML 都会自动注册为该模块的可打开页面,不限于 manifest 指定的那一个。manifest.ui 指定的页面排在首位、作为默认首页。
my_module/ui/
├── index.html # 默认页(manifest.ui 指定)
└── fastboot.html # 自动注册的第二个页面用户在外壳侧边栏展开你的模块,就能在子页面之间切换。页面之间共享同一个后端(RPC、事件都一样),适合"同一设备的不同视图"这类场景。
桥接 SDK
一行引入:
<script src="/jade-bridge.js"></script>| 前端 API | 说明 |
|---|---|
await jade.call(module, method, ...args) | 调用任意模块(含自己)的注册方法,返回 Promise;失败时 reject |
jade.on(topic, fn) | 订阅事件,fn(payload, source);返回取消订阅的函数 |
jade.dialog(如环境提供) | 原生对话框等宿主注入能力,SDK 不会覆盖它 |
建议页面一加载就订阅自己的事件主题。模块在后台推数据,页面自动刷新,不用轮询。
原生 HTML(零依赖)
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<script src="/jade-bridge.js"></script>
</head>
<body>
<button id="btn">刷新</button>
<pre id="out">—</pre>
<script>
async function refresh() {
try {
const data = await jade.call('my_module', 'snapshot');
document.getElementById('out').textContent = JSON.stringify(data, null, 2);
} catch (e) {
document.getElementById('out').textContent = '错误: ' + e.message;
}
}
document.getElementById('btn').onclick = refresh;
jade.on('my_module/update', (payload, source) => {
document.getElementById('out').textContent = JSON.stringify(payload);
});
refresh();
</script>
</body>
</html>Vue 3(本地文件,离线可用)
my_module/ui/
├── index.html
└── vendor/vue.global.prod.js # 自行下载放入,随模块一起打包<div id="app">
<button @click="load">刷新</button>
<pre>{{ text }}</pre>
</div>
<script src="vendor/vue.global.prod.js"></script>
<script src="/jade-bridge.js"></script>
<script>
Vue.createApp({
data: () => ({ text: '—' }),
methods: {
async load() {
try { this.text = JSON.stringify(await jade.call('my_module', 'snapshot')); }
catch (e) { this.text = '错误: ' + e.message; }
}
},
mounted() {
this.load();
jade.on('my_module/update', (p) => this.text = JSON.stringify(p));
}
}).mount('#app');
</script>React(UMD 本地文件)
<div id="root"></div>
<script src="vendor/react.production.min.js"></script>
<script src="vendor/react-dom.production.min.js"></script>
<script src="/jade-bridge.js"></script>
<script>
const { useState, useEffect } = React;
function App() {
const [text, setText] = useState('—');
const load = async () => {
try { setText(JSON.stringify(await jade.call('my_module', 'snapshot'))); }
catch (e) { setText('错误: ' + e.message); }
};
useEffect(() => {
load();
jade.on('my_module/update', p => setText(JSON.stringify(p)));
}, []);
return React.createElement('div', null,
React.createElement('button', { onClick: load }, '刷新'),
React.createElement('pre', null, text));
}
ReactDOM.createRoot(document.getElementById('root')).render(React.createElement(App));
</script>几条硬规矩
- 用户机器可能离线,不要依赖 CDN。第三方库下载后放进
ui/随模块打包。 - iframe 内的相对路径基于
/modules/<模块名>/,写vendor/xxx.js,不要写/vendor/xxx.js。 - 页面只与本地服务通信,跨域请求会被浏览器拦。要访问外网,放到 Python 侧处理(声明
net权限)。 - 前端调用固定 10 秒超时,页面里改不了。长任务改成「立即返回 + 后台执行 + 事件推进度」。
- 页面在模块停用时会调用失败,记得
try/catch。