Skip to content

插件开发常见问题 (FAQ)

UnityAI-Tauri 插件开发常见问题解答

目录


入门问题

什么是云端插件?

云端插件是运行在远程服务器上的 Web 应用,通过 iframe 嵌入到 UnityAI-Tauri 主应用中。与传统的本地插件不同,云端插件具有以下特点:

  • 独立部署:插件代码部署在开发者自己的服务器上
  • 跨平台:使用标准 Web 技术(HTML/CSS/JavaScript)
  • 安全隔离:通过 iframe 沙箱机制与主应用隔离
  • 即时更新:更新插件无需用户重新安装

我需要什么技术栈?

必需技能:

  • HTML/CSS/JavaScript 基础
  • 理解 postMessage API
  • 基本的 HTTP/REST 概念

推荐技能:

  • 现代前端框架(React/Vue/Svelte)
  • TypeScript
  • 前端构建工具(Vite/Webpack)

不需要:

  • 不需要学习 Tauri 或 Rust
  • 不需要了解主应用的内部实现
  • 不需要特定的后端技术栈

如何开始开发第一个插件?

  1. 阅读快速开始指南QUICK_START_CN.md
  2. 查看示例插件
  3. 设置开发环境
    bash
    # 创建项目目录
    mkdir my-plugin
    cd my-plugin
    
    # 创建基本文件
    touch index.html
    touch plugin.js
    touch styles.css
  4. 启动本地服务器
    bash
    # 使用 Python
    python -m http.server 8080
    
    # 或使用 Node.js
    npx serve -p 8080

插件可以做什么?

可以做:

  • 显示自定义 UI 界面
  • 存储和读取用户数据
  • 调用主应用提供的 API
  • 与外部服务通信(需要通过主应用代理)
  • 响应用户交互

不能做:

  • 直接访问用户文件系统
  • 直接访问系统 API
  • 绕过主应用的安全限制
  • 访问其他插件的数据

开发插件需要付费吗?

不需要。插件开发完全免费,包括:

  • 使用插件 API
  • 数据存储服务
  • 技术文档和示例
  • 开发者社区支持

但是,您需要自己承担:

  • 插件托管服务器的费用
  • 域名费用(如果需要)
  • 第三方服务费用(如果使用)

如何测试我的插件?

本地测试:

  1. 启动本地开发服务器
  2. 在主应用中添加插件,URL 使用 http://localhost:8080
  3. 使用浏览器开发者工具调试

生产测试:

  1. 部署到测试服务器
  2. 在主应用中使用测试 URL
  3. 邀请测试用户试用

自动化测试:

  • 单元测试:测试业务逻辑
  • 集成测试:测试 API 调用
  • E2E 测试:测试完整用户流程

详见:测试与调试

开发问题

如何调试插件?

使用浏览器开发者工具:

  1. 在主应用中打开插件
  2. 右键点击插件区域,选择"检查元素"
  3. 在开发者工具中查看:
    • Console:查看日志和错误
    • Network:查看 API 请求
    • Sources:设置断点调试
    • Application:查看存储数据

添加调试日志:

javascript
// 记录所有 postMessage 通信
window.addEventListener('message', (event) => {
  console.log('[Plugin Debug]', {
    type: event.data.type,
    payload: event.data.payload,
    origin: event.origin
  });
});

// 记录 API 调用
function debugApiCall(endpoint, method, body) {
  console.log('[API Call]', { endpoint, method, body });
  // ... 实际调用
}

远程调试:

如果插件部署在远程服务器,可以使用:

  • Chrome DevTools 远程调试
  • 日志服务(如 Sentry、LogRocket)
  • 自定义日志端点

Token 过期怎么办?

检测 Token 过期:

javascript
let authToken = null;
let tokenExpiry = null;

function isTokenExpired() {
  if (!tokenExpiry) return true;
  return Date.now() > tokenExpiry;
}

// 在每次 API 调用前检查
function apiCall(endpoint, method, body) {
  if (isTokenExpired()) {
    requestToken();
    return; // 等待新 Token
  }
  // 继续调用
}

自动刷新 Token:

javascript
// Token 响应处理
window.addEventListener('message', (event) => {
  if (event.data.type === 'token_response') {
    authToken = event.data.payload.token;
    
    // 解析 JWT 获取过期时间
    const payload = JSON.parse(atob(authToken.split('.')[1]));
    tokenExpiry = payload.exp * 1000;
    
    // 提前 5 分钟刷新
    const refreshTime = tokenExpiry - Date.now() - 5 * 60 * 1000;
    setTimeout(() => requestToken(), refreshTime);
  }
});

处理 Token 错误:

javascript
window.addEventListener('message', (event) => {
  if (event.data.type === 'error' && 
      event.data.error.includes('not authenticated')) {
    // Token 无效,重新请求
    requestToken();
  }
});

数据存储有什么限制?

大小限制:

  • 单个 value 建议不超过 1MB
  • 超大数据应该分片存储
  • 总存储空间按用户配额管理

分片存储示例:

javascript
// 保存大数据
function saveLargeData(key, data) {
  const json = JSON.stringify(data);
  const chunkSize = 500 * 1024; // 500KB per chunk
  const chunks = [];
  
  for (let i = 0; i < json.length; i += chunkSize) {
    chunks.push(json.slice(i, i + chunkSize));
  }
  
  // 保存元数据
  saveData(`${key}_meta`, JSON.stringify({
    chunks: chunks.length,
    totalSize: json.length
  }));
  
  // 保存每个分片
  chunks.forEach((chunk, index) => {
    saveData(`${key}_chunk_${index}`, chunk);
  });
}

// 读取大数据
async function loadLargeData(key) {
  const meta = JSON.parse(await getData(`${key}_meta`));
  const chunks = [];
  
  for (let i = 0; i < meta.chunks; i++) {
    chunks.push(await getData(`${key}_chunk_${i}`));
  }
  
  return JSON.parse(chunks.join(''));
}

性能限制:

  • 避免频繁保存(使用防抖)
  • 批量操作优于单个操作
  • 使用缓存减少读取

如何处理跨域问题?

插件不需要处理 CORS:

所有 API 请求都通过主应用代理,主应用会:

  • 自动添加 Authorization header
  • 处理 CORS 预检请求
  • 统一错误处理

正确的 API 调用方式:

javascript
// ✅ 正确:通过 postMessage
window.parent.postMessage({
  type: 'api_call',
  payload: {
    endpoint: '/api/external-service',
    method: 'GET'
  }
}, '*');

// ❌ 错误:直接 fetch(会遇到 CORS)
fetch('https://api.example.com/data')
  .then(res => res.json());

如果必须直接调用外部 API:

  1. 在插件服务器端设置代理
  2. 或者使用 JSONP(不推荐)
  3. 或者请求主应用添加新的代理端点

如何与外部服务集成?

通过主应用代理:

javascript
// 调用外部 API
window.parent.postMessage({
  type: 'api_call',
  payload: {
    endpoint: '/api/proxy/external-service',
    method: 'POST',
    body: {
      query: 'search term'
    }
  }
}, '*');

在插件服务器端集成:

如果外部服务需要复杂的认证或处理:

  1. 在插件服务器端实现 API 端点
  2. 插件前端调用自己的服务器
  3. 服务器端调用外部服务
  4. 返回结果给前端
javascript
// 插件前端
fetch('https://your-plugin-server.com/api/search', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ query: 'search term' })
})
.then(res => res.json())
.then(data => {
  // 处理结果
});

如何实现实时更新?

轮询方式:

javascript
// 定期检查更新
setInterval(async () => {
  const data = await loadData();
  updateUI(data);
}, 5000); // 每 5 秒

长轮询方式:

javascript
async function longPoll() {
  try {
    const response = await apiCall('/api/updates', 'GET');
    if (response.hasUpdate) {
      updateUI(response.data);
    }
  } catch (error) {
    console.error('Long poll error:', error);
  }
  
  // 继续轮询
  setTimeout(longPoll, 1000);
}

WebSocket(需要插件服务器支持):

javascript
// 连接到插件服务器的 WebSocket
const ws = new WebSocket('wss://your-plugin-server.com/ws');

ws.onmessage = (event) => {
  const data = JSON.parse(event.data);
  updateUI(data);
};

ws.onerror = (error) => {
  console.error('WebSocket error:', error);
  // 回退到轮询
};

如何处理大量数据?

虚拟滚动:

javascript
// 只渲染可见区域的数据
function VirtualList({ items, itemHeight, containerHeight }) {
  const [scrollTop, setScrollTop] = useState(0);
  
  const visibleStart = Math.floor(scrollTop / itemHeight);
  const visibleEnd = Math.ceil((scrollTop + containerHeight) / itemHeight);
  const visibleItems = items.slice(visibleStart, visibleEnd);
  
  return (
    <div 
      style={{ height: containerHeight, overflow: 'auto' }}
      onScroll={(e) => setScrollTop(e.target.scrollTop)}
    >
      <div style={{ height: items.length * itemHeight }}>
        <div style={{ transform: `translateY(${visibleStart * itemHeight}px)` }}>
          {visibleItems.map(item => (
            <div key={item.id} style={{ height: itemHeight }}>
              {item.content}
            </div>
          ))}
        </div>
      </div>
    </div>
  );
}

分页加载:

javascript
let currentPage = 0;
const pageSize = 50;

async function loadMore() {
  const data = await loadData();
  const allItems = Object.values(data);
  
  const start = currentPage * pageSize;
  const end = start + pageSize;
  const pageItems = allItems.slice(start, end);
  
  appendToUI(pageItems);
  currentPage++;
}

数据压缩:

javascript
// 使用 LZ-String 压缩
import LZString from 'lz-string';

function saveCompressed(key, data) {
  const json = JSON.stringify(data);
  const compressed = LZString.compress(json);
  saveData(key, compressed);
}

function loadCompressed(key) {
  const compressed = await getData(key);
  const json = LZString.decompress(compressed);
  return JSON.parse(json);
}

部署问题

如何部署插件?

静态托管(推荐):

适合纯前端插件,无需后端服务器。

bash
# 构建生产版本
npm run build

# 部署到静态托管服务
# Vercel
vercel --prod

# Netlify
netlify deploy --prod

# GitHub Pages
git push origin gh-pages

传统服务器:

适合需要后端逻辑的插件。

bash
# 使用 Nginx
server {
    listen 443 ssl;
    server_name your-plugin.com;
    
    ssl_certificate /path/to/cert.pem;
    ssl_certificate_key /path/to/key.pem;
    
    root /var/www/plugin;
    index index.html;
    
    location / {
        try_files $uri $uri/ /index.html;
    }
}

Docker 部署:

dockerfile
FROM nginx:alpine
COPY dist/ /usr/share/nginx/html/
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
bash
# 构建镜像
docker build -t my-plugin .

# 运行容器
docker run -d -p 80:80 my-plugin

如何更新插件?

无缝更新:

云端插件的优势是可以即时更新,无需用户操作。

bash
# 1. 构建新版本
npm run build

# 2. 部署到生产环境
# 用户刷新页面即可看到更新

# 3. 如果使用 CDN,清除缓存
# Cloudflare
curl -X POST "https://api.cloudflare.com/client/v4/zones/{zone_id}/purge_cache" \
  -H "Authorization: Bearer {api_token}" \
  -d '{"purge_everything":true}'

版本管理:

javascript
// 在插件中显示版本号
const PLUGIN_VERSION = '1.2.3';

// 检查是否有新版本
async function checkUpdate() {
  const response = await fetch('https://your-plugin.com/version.json');
  const { version } = await response.json();
  
  if (version !== PLUGIN_VERSION) {
    showUpdateNotification('新版本可用,请刷新页面');
  }
}

灰度发布:

javascript
// 根据用户 ID 决定版本
function getPluginUrl(userId) {
  const hash = hashCode(userId);
  const bucket = hash % 100;
  
  // 10% 用户使用新版本
  if (bucket < 10) {
    return 'https://your-plugin.com/v2/';
  }
  
  return 'https://your-plugin.com/v1/';
}

如何回滚版本?

静态托管回滚:

bash
# Vercel - 回滚到上一个部署
vercel rollback

# Netlify - 在控制台选择历史部署
# 或使用 CLI
netlify deploy --alias previous-version

Docker 回滚:

bash
# 停止当前容器
docker stop my-plugin

# 启动旧版本容器
docker run -d -p 80:80 my-plugin:v1.2.2

Git 回滚:

bash
# 回滚到上一个提交
git revert HEAD

# 或回滚到特定版本
git revert <commit-hash>

# 重新部署
npm run build && npm run deploy

如何配置 HTTPS?

使用 Let's Encrypt(免费):

bash
# 安装 Certbot
sudo apt-get install certbot python3-certbot-nginx

# 获取证书
sudo certbot --nginx -d your-plugin.com

# 自动续期
sudo certbot renew --dry-run

使用 Cloudflare(推荐):

  1. 将域名 DNS 指向 Cloudflare
  2. 在 Cloudflare 控制台启用 SSL/TLS
  3. 选择 "Full" 或 "Full (strict)" 模式
  4. 自动获得 HTTPS

Nginx 配置:

nginx
server {
    listen 80;
    server_name your-plugin.com;
    return 301 https://$server_name$request_uri;
}

server {
    listen 443 ssl http2;
    server_name your-plugin.com;
    
    ssl_certificate /etc/letsencrypt/live/your-plugin.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/your-plugin.com/privkey.pem;
    
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers HIGH:!aNULL:!MD5;
    
    # ... 其他配置
}

如何监控插件运行状态?

前端错误监控:

javascript
// 使用 Sentry
import * as Sentry from "@sentry/browser";

Sentry.init({
  dsn: "your-sentry-dsn",
  environment: "production",
  release: "my-plugin@1.2.3"
});

// 捕获错误
try {
  // 代码
} catch (error) {
  Sentry.captureException(error);
}

性能监控:

javascript
// 监控加载时间
window.addEventListener('load', () => {
  const loadTime = performance.now();
  console.log(`Plugin loaded in ${loadTime}ms`);
  
  // 发送到分析服务
  sendMetric('load_time', loadTime);
});

// 监控 API 调用
function monitoredApiCall(endpoint, method, body) {
  const startTime = performance.now();
  
  return apiCall(endpoint, method, body)
    .then(result => {
      const duration = performance.now() - startTime;
      sendMetric('api_call_duration', duration, { endpoint });
      return result;
    })
    .catch(error => {
      sendMetric('api_call_error', 1, { endpoint, error: error.message });
      throw error;
    });
}

健康检查端点:

javascript
// 在插件服务器端实现
app.get('/health', (req, res) => {
  res.json({
    status: 'ok',
    version: '1.2.3',
    uptime: process.uptime(),
    timestamp: new Date().toISOString()
  });
});

如何处理多环境部署?

环境配置:

javascript
// config.js
const configs = {
  development: {
    apiUrl: 'http://localhost:3000',
    pluginId: 'dev-plugin-id',
    debug: true
  },
  staging: {
    apiUrl: 'https://staging-api.example.com',
    pluginId: 'staging-plugin-id',
    debug: true
  },
  production: {
    apiUrl: 'https://api.example.com',
    pluginId: 'prod-plugin-id',
    debug: false
  }
};

const env = process.env.NODE_ENV || 'development';
export default configs[env];

构建脚本:

json
{
  "scripts": {
    "build:dev": "NODE_ENV=development vite build",
    "build:staging": "NODE_ENV=staging vite build",
    "build:prod": "NODE_ENV=production vite build",
    "deploy:staging": "npm run build:staging && deploy-to-staging.sh",
    "deploy:prod": "npm run build:prod && deploy-to-prod.sh"
  }
}

CI/CD 配置(GitHub Actions):

yaml
name: Deploy Plugin

on:
  push:
    branches:
      - main
      - staging

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      
      - name: Setup Node.js
        uses: actions/setup-node@v2
        with:
          node-version: '18'
      
      - name: Install dependencies
        run: npm ci
      
      - name: Build
        run: |
          if [ "${{ github.ref }}" == "refs/heads/main" ]; then
            npm run build:prod
          else
            npm run build:staging
          fi
      
      - name: Deploy
        run: |
          if [ "${{ github.ref }}" == "refs/heads/main" ]; then
            npm run deploy:prod
          else
            npm run deploy:staging
          fi
        env:
          DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}

安全问题

如何保护用户数据?

数据隔离:

插件系统自动实现数据隔离:

  • 每个插件只能访问自己的数据
  • 数据按 user_id + plugin_id + key 隔离
  • 无法访问其他插件或其他用户的数据

敏感数据处理:

javascript
// ❌ 错误:在 localStorage 存储敏感数据
localStorage.setItem('password', userPassword);
localStorage.setItem('token', authToken);

// ✅ 正确:只在内存中保存
let sensitiveData = null;

function setSensitiveData(data) {
  sensitiveData = data;
  // 不持久化
}

function clearSensitiveData() {
  sensitiveData = null;
}

// 页面卸载时清除
window.addEventListener('beforeunload', clearSensitiveData);

数据加密(计划中):

未来版本将支持端到端加密:

javascript
// 未来 API(示例)
window.parent.postMessage({
  type: 'save_encrypted_data',
  payload: {
    pluginId: PLUGIN_ID,
    key: 'sensitive',
    value: data,
    encryption: 'aes-256-gcm'
  }
}, '*');

如何防止 XSS 攻击?

输入验证:

javascript
// 验证用户输入
function sanitizeInput(input) {
  // 移除 HTML 标签
  const div = document.createElement('div');
  div.textContent = input;
  return div.innerHTML;
}

// 使用
const userInput = getUserInput();
const safe = sanitizeInput(userInput);
element.textContent = safe;

使用 DOMPurify:

javascript
import DOMPurify from 'dompurify';

// 清理 HTML
function renderUserContent(html) {
  const clean = DOMPurify.sanitize(html, {
    ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a'],
    ALLOWED_ATTR: ['href']
  });
  element.innerHTML = clean;
}

Content Security Policy:

在插件服务器端设置 CSP 头:

nginx
add_header Content-Security-Policy "
  default-src 'self';
  script-src 'self' 'unsafe-inline';
  style-src 'self' 'unsafe-inline';
  img-src 'self' data: https:;
  connect-src 'self' https://app.unityai.com;
  frame-ancestors https://app.unityai.com;
" always;

如何处理敏感信息?

环境变量:

javascript
// ❌ 错误:硬编码 API 密钥
const API_KEY = 'sk-1234567890abcdef';

// ✅ 正确:使用环境变量
const API_KEY = import.meta.env.VITE_API_KEY;

// 构建时注入
// .env.production
VITE_API_KEY=sk-prod-key-here

服务器端处理:

敏感操作应该在插件服务器端进行:

javascript
// 插件前端
async function callExternalAPI(query) {
  // 不暴露 API 密钥
  const response = await fetch('https://your-plugin-server.com/api/search', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ query })
  });
  return response.json();
}

// 插件服务器端
app.post('/api/search', async (req, res) => {
  const { query } = req.body;
  
  // 在服务器端使用 API 密钥
  const result = await externalAPI.search(query, {
    apiKey: process.env.EXTERNAL_API_KEY
  });
  
  res.json(result);
});

不要在前端存储密钥:

javascript
// ❌ 绝对不要这样做
const config = {
  apiKey: 'secret-key',
  apiSecret: 'secret-value',
  privateKey: '-----BEGIN PRIVATE KEY-----...'
};

如何验证消息来源?

Origin 验证:

javascript
// 生产环境必须验证 origin
const ALLOWED_ORIGINS = [
  'https://app.unityai.com',
  'https://staging.unityai.com'
];

window.addEventListener('message', (event) => {
  // 验证来源
  if (!ALLOWED_ORIGINS.includes(event.origin)) {
    console.warn('Rejected message from unauthorized origin:', event.origin);
    return;
  }
  
  // 处理消息
  handleMessage(event.data);
});

开发环境配置:

javascript
// 根据环境选择允许的 origin
const getAllowedOrigins = () => {
  if (import.meta.env.DEV) {
    return ['http://localhost:1420', 'http://127.0.0.1:1420'];
  }
  return ['https://app.unityai.com'];
};

const ALLOWED_ORIGINS = getAllowedOrigins();

如何防止 CSRF 攻击?

插件系统自动防护:

主应用已经实现了 CSRF 防护:

  • 所有 API 请求都需要有效的 JWT Token
  • Token 包含用户身份和权限信息
  • Token 有过期时间限制

插件端最佳实践:

javascript
// 不需要额外的 CSRF Token
// JWT Token 已经提供了足够的保护

// 确保每次 API 调用都通过 postMessage
function apiCall(endpoint, method, body) {
  window.parent.postMessage({
    type: 'api_call',
    payload: { endpoint, method, body }
  }, '*');
}

如何处理第三方依赖的安全问题?

定期审计:

bash
# 检查已知漏洞
npm audit

# 自动修复
npm audit fix

# 查看详细报告
npm audit --json

使用 Dependabot:

在 GitHub 仓库中启用 Dependabot:

yaml
# .github/dependabot.yml
version: 2
updates:
  - package-ecosystem: "npm"
    directory: "/"
    schedule:
      interval: "weekly"
    open-pull-requests-limit: 10

锁定依赖版本:

json
{
  "dependencies": {
    "react": "18.2.0",
    "react-dom": "18.2.0"
  }
}

使用 SRI(Subresource Integrity):

如果从 CDN 加载资源:

html
<script 
  src="https://cdn.example.com/library.js"
  integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/ux..."
  crossorigin="anonymous">
</script>

性能问题

如何优化插件加载速度?

代码分割:

javascript
// 使用动态导入
const HeavyComponent = lazy(() => import('./HeavyComponent'));

// 或使用 Vite 的代码分割
const loadChart = () => import('chart.js');

// 按需加载
button.addEventListener('click', async () => {
  const { Chart } = await loadChart();
  new Chart(ctx, config);
});

资源优化:

javascript
// 压缩图片
// 使用 WebP 格式
<img src="image.webp" alt="..." />

// 懒加载图片
<img src="placeholder.jpg" data-src="real-image.jpg" loading="lazy" />

// 预加载关键资源
<link rel="preload" href="critical.js" as="script" />
<link rel="preload" href="font.woff2" as="font" type="font/woff2" crossorigin />

减少包体积:

bash
# 分析包大小
npm run build -- --analyze

# 移除未使用的依赖
npm prune

# 使用更小的替代库
# 例如:day.js 替代 moment.js
npm install dayjs
npm uninstall moment

使用 CDN:

html
<!-- 从 CDN 加载常用库 -->
<script src="https://cdn.jsdelivr.net/npm/react@18/umd/react.production.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/react-dom@18/umd/react-dom.production.min.js"></script>

如何减少 API 调用次数?

批量操作:

javascript
// ❌ 错误:多次调用
for (const item of items) {
  await saveData(`item_${item.id}`, JSON.stringify(item));
}

// ✅ 正确:批量保存
const savePromises = items.map(item => 
  saveData(`item_${item.id}`, JSON.stringify(item))
);
await Promise.all(savePromises);

缓存策略:

javascript
class DataCache {
  constructor(ttl = 60000) {
    this.cache = new Map();
    this.ttl = ttl;
  }
  
  set(key, value) {
    this.cache.set(key, {
      value,
      expiry: Date.now() + this.ttl
    });
  }
  
  get(key) {
    const item = this.cache.get(key);
    if (!item) return null;
    
    if (Date.now() > item.expiry) {
      this.cache.delete(key);
      return null;
    }
    
    return item.value;
  }
  
  clear() {
    this.cache.clear();
  }
}

// 使用缓存
const cache = new DataCache(60000); // 1 分钟

async function getData(key) {
  // 先查缓存
  const cached = cache.get(key);
  if (cached) return cached;
  
  // 缓存未命中,调用 API
  const data = await apiCall(`/api/data/${key}`, 'GET');
  cache.set(key, data);
  return data;
}

防抖和节流:

javascript
// 防抖:延迟执行,适合搜索输入
function debounce(func, wait) {
  let timeout;
  return function(...args) {
    clearTimeout(timeout);
    timeout = setTimeout(() => func.apply(this, args), wait);
  };
}

// 使用
const searchInput = document.getElementById('search');
const debouncedSearch = debounce(async (query) => {
  const results = await apiCall('/api/search', 'POST', { query });
  displayResults(results);
}, 500);

searchInput.addEventListener('input', (e) => {
  debouncedSearch(e.target.value);
});

// 节流:限制频率,适合滚动事件
function throttle(func, limit) {
  let inThrottle;
  return function(...args) {
    if (!inThrottle) {
      func.apply(this, args);
      inThrottle = true;
      setTimeout(() => inThrottle = false, limit);
    }
  };
}

// 使用
const throttledScroll = throttle(() => {
  console.log('Scroll position:', window.scrollY);
}, 1000);

window.addEventListener('scroll', throttledScroll);

请求合并:

javascript
class RequestBatcher {
  constructor(batchFn, delay = 50) {
    this.batchFn = batchFn;
    this.delay = delay;
    this.queue = [];
    this.timer = null;
  }
  
  add(request) {
    return new Promise((resolve, reject) => {
      this.queue.push({ request, resolve, reject });
      
      if (!this.timer) {
        this.timer = setTimeout(() => this.flush(), this.delay);
      }
    });
  }
  
  async flush() {
    const batch = this.queue.splice(0);
    this.timer = null;
    
    try {
      const results = await this.batchFn(batch.map(b => b.request));
      batch.forEach((item, index) => {
        item.resolve(results[index]);
      });
    } catch (error) {
      batch.forEach(item => item.reject(error));
    }
  }
}

// 使用
const batcher = new RequestBatcher(async (requests) => {
  // 批量调用 API
  return await apiCall('/api/batch', 'POST', { requests });
});

// 多个请求会自动合并
const result1 = await batcher.add({ type: 'get', id: 1 });
const result2 = await batcher.add({ type: 'get', id: 2 });

如何处理内存泄漏?

清理事件监听器:

javascript
// ❌ 错误:忘记清理
function setupListener() {
  window.addEventListener('message', handleMessage);
}

// ✅ 正确:组件卸载时清理
function setupListener() {
  const handler = (event) => handleMessage(event);
  window.addEventListener('message', handler);
  
  // 返回清理函数
  return () => {
    window.removeEventListener('message', handler);
  };
}

// React 示例
useEffect(() => {
  const cleanup = setupListener();
  return cleanup;
}, []);

清理定时器:

javascript
// ❌ 错误:定时器未清理
setInterval(() => {
  updateData();
}, 5000);

// ✅ 正确:保存引用并清理
const intervalId = setInterval(() => {
  updateData();
}, 5000);

// 组件卸载时清理
window.addEventListener('beforeunload', () => {
  clearInterval(intervalId);
});

避免闭包陷阱:

javascript
// ❌ 错误:闭包持有大对象
function createHandler(largeData) {
  return function() {
    // 即使只用了 largeData.id,整个对象都被持有
    console.log(largeData.id);
  };
}

// ✅ 正确:只保留需要的数据
function createHandler(largeData) {
  const id = largeData.id; // 只提取需要的
  return function() {
    console.log(id);
  };
}

使用 WeakMap/WeakSet:

javascript
// 使用 WeakMap 避免内存泄漏
const cache = new WeakMap();

function processElement(element) {
  if (cache.has(element)) {
    return cache.get(element);
  }
  
  const result = expensiveOperation(element);
  cache.set(element, result);
  return result;
}

// 当 element 被垃圾回收时,cache 中的条目也会被自动清理

如何优化渲染性能?

虚拟化长列表:

javascript
// 使用 react-window
import { FixedSizeList } from 'react-window';

function VirtualList({ items }) {
  const Row = ({ index, style }) => (
    <div style={style}>
      {items[index].content}
    </div>
  );
  
  return (
    <FixedSizeList
      height={600}
      itemCount={items.length}
      itemSize={50}
      width="100%"
    >
      {Row}
    </FixedSizeList>
  );
}

使用 requestAnimationFrame:

javascript
// ❌ 错误:直接操作 DOM
function updateProgress(value) {
  progressBar.style.width = value + '%';
}

// ✅ 正确:使用 RAF 批量更新
let rafId = null;
let pendingValue = null;

function updateProgress(value) {
  pendingValue = value;
  
  if (!rafId) {
    rafId = requestAnimationFrame(() => {
      progressBar.style.width = pendingValue + '%';
      rafId = null;
      pendingValue = null;
    });
  }
}

减少重排和重绘:

javascript
// ❌ 错误:多次触发重排
element.style.width = '100px';
element.style.height = '100px';
element.style.margin = '10px';

// ✅ 正确:批量修改
element.style.cssText = 'width: 100px; height: 100px; margin: 10px;';

// 或使用 class
element.className = 'optimized-style';

使用 CSS 动画代替 JS:

css
/* ✅ 使用 CSS 动画(GPU 加速) */
.fade-in {
  animation: fadeIn 0.3s ease-in;
}

@keyframes fadeIn {
  from { opacity: 0; }
  to { opacity: 1; }
}

如何监控插件性能?

Performance API:

javascript
// 标记关键时间点
performance.mark('plugin-init-start');

// ... 初始化代码

performance.mark('plugin-init-end');
performance.measure('plugin-init', 'plugin-init-start', 'plugin-init-end');

// 获取测量结果
const measures = performance.getEntriesByType('measure');
measures.forEach(measure => {
  console.log(`${measure.name}: ${measure.duration}ms`);
});

监控 API 调用:

javascript
class PerformanceMonitor {
  constructor() {
    this.metrics = [];
  }
  
  async trackApiCall(name, apiCallFn) {
    const start = performance.now();
    
    try {
      const result = await apiCallFn();
      const duration = performance.now() - start;
      
      this.metrics.push({
        name,
        duration,
        success: true,
        timestamp: Date.now()
      });
      
      return result;
    } catch (error) {
      const duration = performance.now() - start;
      
      this.metrics.push({
        name,
        duration,
        success: false,
        error: error.message,
        timestamp: Date.now()
      });
      
      throw error;
    }
  }
  
  getStats() {
    const stats = {};
    
    this.metrics.forEach(metric => {
      if (!stats[metric.name]) {
        stats[metric.name] = {
          count: 0,
          totalDuration: 0,
          errors: 0
        };
      }
      
      stats[metric.name].count++;
      stats[metric.name].totalDuration += metric.duration;
      if (!metric.success) stats[metric.name].errors++;
    });
    
    // 计算平均值
    Object.keys(stats).forEach(name => {
      stats[name].avgDuration = stats[name].totalDuration / stats[name].count;
    });
    
    return stats;
  }
}

// 使用
const monitor = new PerformanceMonitor();

const data = await monitor.trackApiCall('loadData', () => 
  apiCall('/api/data', 'GET')
);

// 定期报告
setInterval(() => {
  console.log('Performance Stats:', monitor.getStats());
}, 60000);

内存监控:

javascript
// 监控内存使用(仅 Chrome)
if (performance.memory) {
  setInterval(() => {
    const memory = performance.memory;
    console.log('Memory Usage:', {
      used: (memory.usedJSHeapSize / 1048576).toFixed(2) + ' MB',
      total: (memory.totalJSHeapSize / 1048576).toFixed(2) + ' MB',
      limit: (memory.jsHeapSizeLimit / 1048576).toFixed(2) + ' MB'
    });
  }, 30000);
}

故障排查

插件无法加载怎么办?

检查清单:

  1. 验证 URL 是否正确

    javascript
    // 确保 URL 可访问
    fetch('https://your-plugin.com')
      .then(res => console.log('Status:', res.status))
      .catch(err => console.error('Error:', err));
  2. 检查 HTTPS 配置

    • 生产环境必须使用 HTTPS
    • 证书必须有效(不能是自签名)
    • 检查证书是否过期
  3. 检查 CORS 设置

    nginx
    # Nginx 配置
    add_header Access-Control-Allow-Origin "https://app.unityai.com" always;
    add_header Access-Control-Allow-Methods "GET, POST, OPTIONS" always;
    add_header Access-Control-Allow-Headers "Content-Type, Authorization" always;
  4. 检查 iframe 兼容性

    nginx
    # 允许被 iframe 嵌入
    add_header X-Frame-Options "ALLOW-FROM https://app.unityai.com" always;
    # 或使用 CSP
    add_header Content-Security-Policy "frame-ancestors https://app.unityai.com" always;
  5. 查看浏览器控制台

    • 打开开发者工具
    • 查看 Console 和 Network 标签
    • 查找错误信息

数据保存失败怎么办?

常见原因和解决方案:

1. Token 过期

javascript
// 检查 Token 是否有效
function isTokenValid(token) {
  if (!token) return false;
  
  try {
    const payload = JSON.parse(atob(token.split('.')[1]));
    return payload.exp * 1000 > Date.now();
  } catch {
    return false;
  }
}

// 在保存前检查
if (!isTokenValid(authToken)) {
  await requestToken();
}

2. 数据过大

javascript
// 检查数据大小
function checkDataSize(data) {
  const size = new Blob([JSON.stringify(data)]).size;
  const maxSize = 1024 * 1024; // 1MB
  
  if (size > maxSize) {
    console.error(`Data too large: ${size} bytes (max: ${maxSize})`);
    return false;
  }
  
  return true;
}

3. 网络问题

javascript
// 添加重试逻辑
async function saveDataWithRetry(key, value, maxRetries = 3) {
  for (let i = 0; i < maxRetries; i++) {
    try {
      await saveData(key, value);
      return;
    } catch (error) {
      console.warn(`Save attempt ${i + 1} failed:`, error);
      
      if (i === maxRetries - 1) {
        throw error;
      }
      
      // 指数退避
      await new Promise(resolve => 
        setTimeout(resolve, Math.pow(2, i) * 1000)
      );
    }
  }
}

API 调用返回错误怎么办?

错误码对照表:

错误码含义解决方案
401未授权重新请求 Token
403权限不足检查插件权限配置
404端点不存在检查 API 路径是否正确
429请求过多添加限流或延迟重试
500服务器错误联系技术支持

错误处理模板:

javascript
async function handleApiError(error, operation) {
  console.error(`API Error in ${operation}:`, error);
  
  // 根据错误类型处理
  if (error.message.includes('401') || error.message.includes('not authenticated')) {
    // Token 问题
    console.log('Token expired, requesting new token...');
    await requestToken();
    return 'retry';
  }
  
  if (error.message.includes('429') || error.message.includes('Rate limit')) {
    // 限流
    console.log('Rate limited, waiting before retry...');
    await new Promise(resolve => setTimeout(resolve, 5000));
    return 'retry';
  }
  
  if (error.message.includes('500') || error.message.includes('503')) {
    // 服务器错误
    console.error('Server error, please try again later');
    showErrorToUser('服务暂时不可用,请稍后重试');
    return 'abort';
  }
  
  // 其他错误
  showErrorToUser(`操作失败: ${error.message}`);
  return 'abort';
}

// 使用
try {
  await apiCall('/api/data', 'GET');
} catch (error) {
  const action = await handleApiError(error, 'loadData');
  if (action === 'retry') {
    // 重试操作
  }
}

插件在某些浏览器上不工作怎么办?

浏览器兼容性检查:

javascript
// 检测浏览器特性
function checkBrowserCompatibility() {
  const issues = [];
  
  // 检查 postMessage
  if (!window.postMessage) {
    issues.push('postMessage API not supported');
  }
  
  // 检查 localStorage
  try {
    localStorage.setItem('test', 'test');
    localStorage.removeItem('test');
  } catch {
    issues.push('localStorage not available');
  }
  
  // 检查 fetch
  if (!window.fetch) {
    issues.push('Fetch API not supported');
  }
  
  // 检查 Promise
  if (!window.Promise) {
    issues.push('Promise not supported');
  }
  
  if (issues.length > 0) {
    console.error('Browser compatibility issues:', issues);
    showErrorToUser('您的浏览器版本过低,请升级到最新版本');
    return false;
  }
  
  return true;
}

// 在插件初始化时检查
if (!checkBrowserCompatibility()) {
  // 显示错误提示
}

Polyfill 支持:

html
<!-- 添加 polyfill -->
<script src="https://cdn.jsdelivr.net/npm/core-js-bundle@3/minified.js"></script>
<script src="https://cdn.jsdelivr.net/npm/whatwg-fetch@3/dist/fetch.umd.js"></script>

浏览器特定问题:

javascript
// Safari 特定问题
const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);

if (isSafari) {
  // Safari 可能阻止第三方 cookie
  console.warn('Safari detected, some features may be limited');
}

// IE11 特定问题(如果需要支持)
const isIE11 = !!window.MSInputMethodContext && !!document.documentMode;

if (isIE11) {
  console.error('IE11 is not supported');
  showErrorToUser('不支持 IE11,请使用现代浏览器');
}

如何调试 postMessage 通信问题?

消息拦截器:

javascript
// 拦截所有发送的消息
const originalPostMessage = window.parent.postMessage;
window.parent.postMessage = function(message, targetOrigin) {
  console.log('[Outgoing Message]', {
    type: message.type,
    payload: message.payload,
    targetOrigin,
    timestamp: new Date().toISOString()
  });
  
  return originalPostMessage.call(window.parent, message, targetOrigin);
};

// 拦截所有接收的消息
window.addEventListener('message', (event) => {
  console.log('[Incoming Message]', {
    type: event.data.type,
    payload: event.data.payload,
    origin: event.origin,
    timestamp: new Date().toISOString()
  });
}, true); // 使用捕获阶段

消息队列监控:

javascript
class MessageMonitor {
  constructor() {
    this.sentMessages = [];
    this.receivedMessages = [];
    this.pendingMessages = new Map();
  }
  
  trackSent(message) {
    const id = Date.now() + Math.random();
    this.sentMessages.push({ id, message, timestamp: Date.now() });
    this.pendingMessages.set(message.type, id);
    return id;
  }
  
  trackReceived(message) {
    this.receivedMessages.push({ message, timestamp: Date.now() });
    
    // 检查是否有对应的发送消息
    if (this.pendingMessages.has(message.type)) {
      const sentId = this.pendingMessages.get(message.type);
      const sent = this.sentMessages.find(m => m.id === sentId);
      
      if (sent) {
        const latency = Date.now() - sent.timestamp;
        console.log(`Message roundtrip: ${message.type} took ${latency}ms`);
      }
      
      this.pendingMessages.delete(message.type);
    }
  }
  
  checkPending() {
    const now = Date.now();
    const timeout = 5000; // 5 秒
    
    this.pendingMessages.forEach((id, type) => {
      const sent = this.sentMessages.find(m => m.id === id);
      if (sent && now - sent.timestamp > timeout) {
        console.warn(`Message timeout: ${type} sent ${now - sent.timestamp}ms ago`);
      }
    });
  }
}

// 使用
const monitor = new MessageMonitor();

// 发送消息时
const id = monitor.trackSent({ type: 'get_data', payload: { pluginId } });

// 接收消息时
window.addEventListener('message', (event) => {
  monitor.trackReceived(event.data);
});

// 定期检查超时
setInterval(() => monitor.checkPending(), 1000);

性能问题如何排查?

性能分析工具:

javascript
class PerformanceProfiler {
  constructor() {
    this.marks = new Map();
    this.measures = [];
  }
  
  start(name) {
    this.marks.set(name, performance.now());
  }
  
  end(name) {
    const startTime = this.marks.get(name);
    if (!startTime) {
      console.warn(`No start mark for: ${name}`);
      return;
    }
    
    const duration = performance.now() - startTime;
    this.measures.push({ name, duration, timestamp: Date.now() });
    this.marks.delete(name);
    
    // 警告慢操作
    if (duration > 1000) {
      console.warn(`Slow operation: ${name} took ${duration}ms`);
    }
    
    return duration;
  }
  
  report() {
    console.table(this.measures);
    
    // 按持续时间排序
    const sorted = [...this.measures].sort((a, b) => b.duration - a.duration);
    console.log('Top 5 slowest operations:');
    sorted.slice(0, 5).forEach(m => {
      console.log(`  ${m.name}: ${m.duration.toFixed(2)}ms`);
    });
  }
}

// 使用
const profiler = new PerformanceProfiler();

profiler.start('loadData');
await loadData();
profiler.end('loadData');

profiler.start('renderUI');
renderUI();
profiler.end('renderUI');

// 查看报告
profiler.report();

内存泄漏检测:

javascript
// 监控内存使用
class MemoryMonitor {
  constructor(interval = 10000) {
    this.samples = [];
    this.interval = interval;
    this.timer = null;
  }
  
  start() {
    this.timer = setInterval(() => {
      if (performance.memory) {
        this.samples.push({
          used: performance.memory.usedJSHeapSize,
          total: performance.memory.totalJSHeapSize,
          timestamp: Date.now()
        });
        
        // 只保留最近 100 个样本
        if (this.samples.length > 100) {
          this.samples.shift();
        }
        
        // 检测内存泄漏
        this.checkLeak();
      }
    }, this.interval);
  }
  
  stop() {
    if (this.timer) {
      clearInterval(this.timer);
      this.timer = null;
    }
  }
  
  checkLeak() {
    if (this.samples.length < 10) return;
    
    // 检查最近 10 个样本的趋势
    const recent = this.samples.slice(-10);
    const first = recent[0].used;
    const last = recent[recent.length - 1].used;
    const growth = last - first;
    const growthRate = growth / first;
    
    // 如果内存增长超过 50%
    if (growthRate > 0.5) {
      console.warn('Potential memory leak detected!', {
        growth: (growth / 1048576).toFixed(2) + ' MB',
        growthRate: (growthRate * 100).toFixed(2) + '%'
      });
    }
  }
  
  report() {
    if (this.samples.length === 0) return;
    
    const latest = this.samples[this.samples.length - 1];
    console.log('Memory Usage:', {
      used: (latest.used / 1048576).toFixed(2) + ' MB',
      total: (latest.total / 1048576).toFixed(2) + ' MB'
    });
  }
}

// 使用
const memMonitor = new MemoryMonitor(10000);
memMonitor.start();

// 定期查看报告
setInterval(() => memMonitor.report(), 60000);

如何获取帮助?

自助资源:

  1. 查看文档

  2. 查看示例

  3. 搜索已知问题

联系支持:

  1. 技术支持邮箱

    • support@unityai.com
    • 包含以下信息:
      • 插件 ID 和版本
      • 错误信息和截图
      • 复现步骤
      • 浏览器和操作系统版本
  2. 开发者社区

  3. 提交 Bug

调试信息收集:

javascript
// 生成调试报告
function generateDebugReport() {
  const report = {
    // 插件信息
    plugin: {
      id: PLUGIN_ID,
      version: PLUGIN_VERSION,
      url: window.location.href
    },
    
    // 浏览器信息
    browser: {
      userAgent: navigator.userAgent,
      language: navigator.language,
      platform: navigator.platform,
      cookieEnabled: navigator.cookieEnabled
    },
    
    // 性能信息
    performance: {
      memory: performance.memory ? {
        used: performance.memory.usedJSHeapSize,
        total: performance.memory.totalJSHeapSize
      } : 'not available',
      timing: performance.timing
    },
    
    // 错误日志
    errors: window.errorLog || [],
    
    // 时间戳
    timestamp: new Date().toISOString()
  };
  
  return JSON.stringify(report, null, 2);
}

// 复制到剪贴板
function copyDebugReport() {
  const report = generateDebugReport();
  navigator.clipboard.writeText(report)
    .then(() => alert('调试信息已复制到剪贴板'))
    .catch(err => console.error('Failed to copy:', err));
}

// 添加按钮到 UI
const debugButton = document.createElement('button');
debugButton.textContent = '生成调试报告';
debugButton.onclick = copyDebugReport;
document.body.appendChild(debugButton);

相关资源

获取帮助

HAMA | 蛤蟆数字