移动端插件适配
Obsidian 在 iOS 和 Android 上都有原生应用,但移动端与桌面端存在显著差异。本文介绍如何为你的插件添加移动端支持。
桌面端与移动端差异
核心差异
| 特性 | 桌面端 | 移动端 |
|---|---|---|
| Node.js API | ✅ 完整 | ❌ 不可用 |
| Electron API | ✅ 可用 | ❌ 不可用 |
| 文件系统访问 | ✅ 完整 | ⚠️ 受限 |
| 子进程 | ✅ child_process | ❌ 不可用 |
| 本地服务器 | ✅ 可创建 | ❌ 不可用 |
| 剪贴板 API | ✅ navigator.clipboard | ⚠️ 受限 |
| 屏幕尺寸 | 大 | 小 |
| 输入方式 | 键盘鼠标 | 触屏 |
API 可用性对比
typescript
// ❌ 移动端不可用
const { exec } = require("child_process");
const fs = require("fs");
const path = require("path");
const electron = require("electron");
// ✅ 移动端可用
import { FileSystemAdapter, Vault } from "obsidian";
import { Platform } from "obsidian";检测运行平台
使用 Platform API
typescript
import { Platform } from "obsidian";
// 检测是否为移动端
if (Platform.isMobile) {
// 移动端特定逻辑
}
// 检测具体平台
if (Platform.isIos) {
// iOS 特定逻辑
}
if (Platform.isAndroid) {
// Android 特定逻辑
}
if (Platform.isDesktop) {
// 桌面端特定逻辑
}
if (Platform.isMacOS) {
// macOS 特定逻辑
}在 manifest.json 中声明
json
{
"id": "my-plugin",
"name": "My Plugin",
"version": "1.0.0",
"minAppVersion": "1.0.0",
"isDesktopOnly": false
}重要
如果 isDesktopOnly 设为 true,移动端用户将无法安装你的插件。只有在使用了 Electron 或 Node.js 特有 API 且无法降级时才设为 true。
常见适配场景
文件系统操作
桌面端可以直接使用 Node.js 的 fs 模块,移动端需要使用 Obsidian API:
typescript
import { Platform, FileSystemAdapter, TFile, TFolder } from "obsidian";
class MyPlugin extends Plugin {
async readExternalFile(path: string): Promise<string> {
if (Platform.isDesktop) {
// 桌面端:使用 Node.js API
const { readFileSync } = require("fs");
return readFileSync(path, "utf-8");
} else {
// 移动端:仅能访问仓库内文件
const file = this.app.vault.getAbstractFileByPath(path);
if (file instanceof TFile) {
return await this.app.vault.read(file);
}
throw new Error(`File not found: ${path}`);
}
}
async writeExternalFile(path: string, content: string): Promise<void> {
if (Platform.isDesktop) {
const { writeFileSync } = require("fs");
writeFileSync(path, content, "utf-8");
} else {
// 移动端:只能写入仓库内文件
const file = this.app.vault.getAbstractFileByPath(path);
if (file instanceof TFile) {
await this.app.vault.modify(file, content);
} else {
await this.app.vault.create(path, content);
}
}
}
}子进程与命令执行
移动端无法执行系统命令,需要提供替代方案:
typescript
import { Platform } from "obsidian";
class MyPlugin extends Plugin {
async executeCommand(command: string): Promise<string> {
if (Platform.isMobile) {
// 移动端:使用内置逻辑替代
return this.executeCommandFallback(command);
}
// 桌面端:使用 child_process
const { exec } = require("child_process");
return new Promise((resolve, reject) => {
exec(command, (error: Error | null, stdout: string) => {
if (error) reject(error);
else resolve(stdout);
});
});
}
private async executeCommandFallback(command: string): Promise<string> {
// 使用 Obsidian API 实现替代逻辑
new Notice("此功能在移动端不可用,使用替代方案");
return "";
}
}网络请求
移动端的网络请求需要使用 requestUrl API:
typescript
import { requestUrl, RequestUrlParam } from "obsidian";
async function fetchData(url: string): Promise<any> {
// ✅ 跨平台网络请求
const response = await requestUrl({
url: url,
method: "GET",
headers: {
"Content-Type": "application/json",
},
});
return response.json;
}
// ❌ 避免使用 fetch(部分移动端环境可能不支持)
async function fetchDataNotRecommended(url: string) {
const response = await fetch(url); // 可能在移动端失败
return response.json();
}剪贴板操作
typescript
async function copyToClipboard(text: string): Promise<void> {
if (Platform.isMobile) {
// 移动端:使用 Obsidian API
await navigator.clipboard.writeText(text);
} else {
// 桌面端:可以使用 Electron clipboard
const { clipboard } = require("electron");
clipboard.writeText(text);
}
}UI 适配
响应式布局
移动端屏幕小,需要调整布局:
typescript
class MyView extends ItemView {
async onOpen() {
const container = this.containerEl.children[1] as HTMLElement;
if (Platform.isMobile) {
// 移动端:紧凑布局
container.classList.add("mobile-layout");
} else {
// 桌面端:宽松布局
container.classList.add("desktop-layout");
}
}
}CSS 适配
css
/* 基础样式 */
.my-plugin-container {
padding: 16px;
}
/* 移动端适配 */
.is-mobile .my-plugin-container {
padding: 8px;
font-size: 14px;
}
.is-mobile .my-plugin-sidebar {
width: 100%;
}
.is-mobile .my-plugin-two-column {
flex-direction: column;
}
/* iOS 安全区域 */
.is-ios .my-plugin-container {
padding-bottom: env(safe-area-inset-bottom);
}触摸操作
typescript
// 为移动端添加触摸事件
class TouchHandler {
private element: HTMLElement;
private longPressTimer: number | null = null;
constructor(element: HTMLElement) {
this.element = element;
this.setupTouchHandlers();
}
private setupTouchHandlers() {
if (Platform.isMobile) {
// 长按触发上下文菜单
this.element.addEventListener("touchstart", (e) => {
this.longPressTimer = window.setTimeout(() => {
this.showContextMenu(e);
}, 500);
});
this.element.addEventListener("touchend", () => {
if (this.longPressTimer) {
clearTimeout(this.longPressTimer);
}
});
this.element.addEventListener("touchmove", () => {
if (this.longPressTimer) {
clearTimeout(this.longPressTimer);
}
});
} else {
// 桌面端:右键菜单
this.element.addEventListener("contextmenu", (e) => {
this.showContextMenu(e);
});
}
}
}性能优化
减少内存使用
移动端设备内存有限,需要特别注意:
typescript
// ❌ 不好的做法:缓存大量数据
const allNotesCache: Map<string, string> = new Map();
// ✅ 好的做法:按需加载
class LazyLoader {
private cache = new Map<string, string>();
private maxCacheSize = Platform.isMobile ? 50 : 200;
async getNoteContent(path: string): Promise<string> {
if (this.cache.has(path)) {
return this.cache.get(path)!;
}
const file = this.app.vault.getAbstractFileByPath(path);
if (!(file instanceof TFile)) throw new Error("File not found");
const content = await this.app.vault.read(file);
// LRU 缓存策略
if (this.cache.size >= this.maxCacheSize) {
const firstKey = this.cache.keys().next().value;
if (firstKey) this.cache.delete(firstKey);
}
this.cache.set(path, content);
return content;
}
}节流与防抖
typescript
function debounce<T extends (...args: any[]) => any>(
func: T,
wait: number
): (...args: Parameters<T>) => void {
let timeout: number | null = null;
return function (...args: Parameters<T>) {
if (timeout) clearTimeout(timeout);
timeout = window.setTimeout(() => func(...args), wait);
};
}
// 移动端使用更长的防抖时间
const DEBOUNCE_TIME = Platform.isMobile ? 500 : 200;
const debouncedSearch = debounce(performSearch, DEBOUNCE_TIME);懒加载
typescript
class MyView extends ItemView {
private isLoading = false;
async onOpen() {
// 只加载可见区域的数据
await this.loadVisibleData();
this.setupScrollObserver();
}
private setupScrollObserver() {
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
this.loadItem(entry.target.dataset.path!);
}
});
},
{ rootMargin: Platform.isMobile ? "200px" : "400px" }
);
this.containerEl.querySelectorAll(".lazy-item").forEach((el) => {
observer.observe(el);
});
}
}功能降级策略
渐进增强
typescript
class FeatureManager {
get availableFeatures(): string[] {
const features = ["basic-search", "tags"];
if (Platform.isDesktop) {
features.push("local-server", "external-scripts", "git-integration");
}
if ("Notification" in window) {
features.push("notifications");
}
if ("share" in navigator)) {
features.push("web-share");
}
return features;
}
isFeatureAvailable(feature: string): boolean {
return this.availableFeatures.includes(feature);
}
}优雅降级
typescript
class ShareManager {
async shareContent(title: string, text: string, url?: string): Promise<void> {
// 优先使用 Web Share API(移动端友好)
if (navigator.share) {
try {
await navigator.share({ title, text, url });
return;
} catch (e) {
// 用户取消分享
if (e instanceof Error && e.name === "AbortError") return;
}
}
// 降级:复制到剪贴板
await navigator.clipboard.writeText(text);
new Notice("内容已复制到剪贴板");
}
}测试移动端适配
桌面端模拟
- 打开开发者工具 (
Ctrl/Cmd + Shift + I) - 切换设备模拟模式
- 选择手机型号
Obsidian 移动端调试
iOS 调试步骤:
- 在 Mac 上打开 Safari → 开发 → 你的 iOS 设备
- 选择 Obsidian WebView
- 在控制台中调试
Android 调试步骤:
- 在 Chrome 中打开
chrome://inspect - 找到 Obsidian WebView
- 点击 inspect
兼容性测试清单
markdown
## 移动端测试清单
### 安装与加载
- [ ] 插件在移动端正常加载
- [ ] 无控制台错误
- [ ] 设置页面正常显示
### 功能测试
- [ ] 核心功能正常工作
- [ ] 降级功能正常工作
- [ ] 网络请求正常
- [ ] 文件操作正常
### UI 测试
- [ ] 界面不超出屏幕
- [ ] 按钮大小适合触摸
- [ ] 文字大小可读
- [ ] 滚动流畅
### 性能测试
- [ ] 启动时间 < 2 秒
- [ ] 无明显卡顿
- [ ] 内存占用合理
- [ ] 电池消耗正常
### 兼容性
- [ ] iOS 测试通过
- [ ] Android 测试通过
- [ ] iPad 测试通过
- [ ] 不同屏幕尺寸适配常见问题
插件在移动端加载失败?
检查是否使用了桌面端专有 API:
- 搜索
require("electron")和require("fs") - 确保所有 Node.js API 调用有移动端替代
- 检查
manifest.json中isDesktopOnly设置
移动端通知不工作?
typescript
// 使用 Obsidian 的 Notice 替代系统通知
import { Notice } from "obsidian";
new Notice("操作成功");移动端键盘遮挡内容?
css
/* iOS 键盘适配 */
.is-ios .input-container {
position: fixed;
bottom: 0;
left: 0;
right: 0;
z-index: 100;
transition: transform 0.3s ease;
}相关资源
- 插件开发入门 — 开发环境搭建
- 移动端使用 — 移动端基础使用
- 移动端进阶技巧 — 移动端高级功能
- 性能优化完整指南 — 性能优化策略
- Obsidian 移动端 API — 官方文档