插件开发常见问题 (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
- 不需要了解主应用的内部实现
- 不需要特定的后端技术栈
如何开始开发第一个插件?
- 阅读快速开始指南:QUICK_START_CN.md
- 查看示例插件:
- minimal-plugin - 最简单的插件示例
- data-storage-plugin - 数据存储示例
- 设置开发环境:bash
# 创建项目目录 mkdir my-plugin cd my-plugin # 创建基本文件 touch index.html touch plugin.js touch styles.css - 启动本地服务器:bash
# 使用 Python python -m http.server 8080 # 或使用 Node.js npx serve -p 8080
插件可以做什么?
可以做:
- 显示自定义 UI 界面
- 存储和读取用户数据
- 调用主应用提供的 API
- 与外部服务通信(需要通过主应用代理)
- 响应用户交互
不能做:
- 直接访问用户文件系统
- 直接访问系统 API
- 绕过主应用的安全限制
- 访问其他插件的数据
开发插件需要付费吗?
不需要。插件开发完全免费,包括:
- 使用插件 API
- 数据存储服务
- 技术文档和示例
- 开发者社区支持
但是,您需要自己承担:
- 插件托管服务器的费用
- 域名费用(如果需要)
- 第三方服务费用(如果使用)
如何测试我的插件?
本地测试:
- 启动本地开发服务器
- 在主应用中添加插件,URL 使用
http://localhost:8080 - 使用浏览器开发者工具调试
生产测试:
- 部署到测试服务器
- 在主应用中使用测试 URL
- 邀请测试用户试用
自动化测试:
- 单元测试:测试业务逻辑
- 集成测试:测试 API 调用
- E2E 测试:测试完整用户流程
详见:测试与调试
开发问题
如何调试插件?
使用浏览器开发者工具:
- 在主应用中打开插件
- 右键点击插件区域,选择"检查元素"
- 在开发者工具中查看:
- Console:查看日志和错误
- Network:查看 API 请求
- Sources:设置断点调试
- Application:查看存储数据
添加调试日志:
// 记录所有 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 过期:
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:
// 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 错误:
window.addEventListener('message', (event) => {
if (event.data.type === 'error' &&
event.data.error.includes('not authenticated')) {
// Token 无效,重新请求
requestToken();
}
});数据存储有什么限制?
大小限制:
- 单个 value 建议不超过 1MB
- 超大数据应该分片存储
- 总存储空间按用户配额管理
分片存储示例:
// 保存大数据
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 调用方式:
// ✅ 正确:通过 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:
- 在插件服务器端设置代理
- 或者使用 JSONP(不推荐)
- 或者请求主应用添加新的代理端点
如何与外部服务集成?
通过主应用代理:
// 调用外部 API
window.parent.postMessage({
type: 'api_call',
payload: {
endpoint: '/api/proxy/external-service',
method: 'POST',
body: {
query: 'search term'
}
}
}, '*');在插件服务器端集成:
如果外部服务需要复杂的认证或处理:
- 在插件服务器端实现 API 端点
- 插件前端调用自己的服务器
- 服务器端调用外部服务
- 返回结果给前端
// 插件前端
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 => {
// 处理结果
});如何实现实时更新?
轮询方式:
// 定期检查更新
setInterval(async () => {
const data = await loadData();
updateUI(data);
}, 5000); // 每 5 秒长轮询方式:
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(需要插件服务器支持):
// 连接到插件服务器的 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);
// 回退到轮询
};如何处理大量数据?
虚拟滚动:
// 只渲染可见区域的数据
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>
);
}分页加载:
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++;
}数据压缩:
// 使用 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);
}部署问题
如何部署插件?
静态托管(推荐):
适合纯前端插件,无需后端服务器。
# 构建生产版本
npm run build
# 部署到静态托管服务
# Vercel
vercel --prod
# Netlify
netlify deploy --prod
# GitHub Pages
git push origin gh-pages传统服务器:
适合需要后端逻辑的插件。
# 使用 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 部署:
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;"]# 构建镜像
docker build -t my-plugin .
# 运行容器
docker run -d -p 80:80 my-plugin如何更新插件?
无缝更新:
云端插件的优势是可以即时更新,无需用户操作。
# 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}'版本管理:
// 在插件中显示版本号
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('新版本可用,请刷新页面');
}
}灰度发布:
// 根据用户 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/';
}如何回滚版本?
静态托管回滚:
# Vercel - 回滚到上一个部署
vercel rollback
# Netlify - 在控制台选择历史部署
# 或使用 CLI
netlify deploy --alias previous-versionDocker 回滚:
# 停止当前容器
docker stop my-plugin
# 启动旧版本容器
docker run -d -p 80:80 my-plugin:v1.2.2Git 回滚:
# 回滚到上一个提交
git revert HEAD
# 或回滚到特定版本
git revert <commit-hash>
# 重新部署
npm run build && npm run deploy如何配置 HTTPS?
使用 Let's Encrypt(免费):
# 安装 Certbot
sudo apt-get install certbot python3-certbot-nginx
# 获取证书
sudo certbot --nginx -d your-plugin.com
# 自动续期
sudo certbot renew --dry-run使用 Cloudflare(推荐):
- 将域名 DNS 指向 Cloudflare
- 在 Cloudflare 控制台启用 SSL/TLS
- 选择 "Full" 或 "Full (strict)" 模式
- 自动获得 HTTPS
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;
# ... 其他配置
}如何监控插件运行状态?
前端错误监控:
// 使用 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);
}性能监控:
// 监控加载时间
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;
});
}健康检查端点:
// 在插件服务器端实现
app.get('/health', (req, res) => {
res.json({
status: 'ok',
version: '1.2.3',
uptime: process.uptime(),
timestamp: new Date().toISOString()
});
});如何处理多环境部署?
环境配置:
// 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];构建脚本:
{
"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):
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隔离 - 无法访问其他插件或其他用户的数据
敏感数据处理:
// ❌ 错误:在 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);数据加密(计划中):
未来版本将支持端到端加密:
// 未来 API(示例)
window.parent.postMessage({
type: 'save_encrypted_data',
payload: {
pluginId: PLUGIN_ID,
key: 'sensitive',
value: data,
encryption: 'aes-256-gcm'
}
}, '*');如何防止 XSS 攻击?
输入验证:
// 验证用户输入
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:
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 头:
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;如何处理敏感信息?
环境变量:
// ❌ 错误:硬编码 API 密钥
const API_KEY = 'sk-1234567890abcdef';
// ✅ 正确:使用环境变量
const API_KEY = import.meta.env.VITE_API_KEY;
// 构建时注入
// .env.production
VITE_API_KEY=sk-prod-key-here服务器端处理:
敏感操作应该在插件服务器端进行:
// 插件前端
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);
});不要在前端存储密钥:
// ❌ 绝对不要这样做
const config = {
apiKey: 'secret-key',
apiSecret: 'secret-value',
privateKey: '-----BEGIN PRIVATE KEY-----...'
};如何验证消息来源?
Origin 验证:
// 生产环境必须验证 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);
});开发环境配置:
// 根据环境选择允许的 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 有过期时间限制
插件端最佳实践:
// 不需要额外的 CSRF Token
// JWT Token 已经提供了足够的保护
// 确保每次 API 调用都通过 postMessage
function apiCall(endpoint, method, body) {
window.parent.postMessage({
type: 'api_call',
payload: { endpoint, method, body }
}, '*');
}如何处理第三方依赖的安全问题?
定期审计:
# 检查已知漏洞
npm audit
# 自动修复
npm audit fix
# 查看详细报告
npm audit --json使用 Dependabot:
在 GitHub 仓库中启用 Dependabot:
# .github/dependabot.yml
version: 2
updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 10锁定依赖版本:
{
"dependencies": {
"react": "18.2.0",
"react-dom": "18.2.0"
}
}使用 SRI(Subresource Integrity):
如果从 CDN 加载资源:
<script
src="https://cdn.example.com/library.js"
integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/ux..."
crossorigin="anonymous">
</script>性能问题
如何优化插件加载速度?
代码分割:
// 使用动态导入
const HeavyComponent = lazy(() => import('./HeavyComponent'));
// 或使用 Vite 的代码分割
const loadChart = () => import('chart.js');
// 按需加载
button.addEventListener('click', async () => {
const { Chart } = await loadChart();
new Chart(ctx, config);
});资源优化:
// 压缩图片
// 使用 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 />减少包体积:
# 分析包大小
npm run build -- --analyze
# 移除未使用的依赖
npm prune
# 使用更小的替代库
# 例如:day.js 替代 moment.js
npm install dayjs
npm uninstall moment使用 CDN:
<!-- 从 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 调用次数?
批量操作:
// ❌ 错误:多次调用
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);缓存策略:
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;
}防抖和节流:
// 防抖:延迟执行,适合搜索输入
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);请求合并:
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 });如何处理内存泄漏?
清理事件监听器:
// ❌ 错误:忘记清理
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;
}, []);清理定时器:
// ❌ 错误:定时器未清理
setInterval(() => {
updateData();
}, 5000);
// ✅ 正确:保存引用并清理
const intervalId = setInterval(() => {
updateData();
}, 5000);
// 组件卸载时清理
window.addEventListener('beforeunload', () => {
clearInterval(intervalId);
});避免闭包陷阱:
// ❌ 错误:闭包持有大对象
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:
// 使用 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 中的条目也会被自动清理如何优化渲染性能?
虚拟化长列表:
// 使用 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:
// ❌ 错误:直接操作 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;
});
}
}减少重排和重绘:
// ❌ 错误:多次触发重排
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 动画(GPU 加速) */
.fade-in {
animation: fadeIn 0.3s ease-in;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}如何监控插件性能?
Performance API:
// 标记关键时间点
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 调用:
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);内存监控:
// 监控内存使用(仅 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);
}故障排查
插件无法加载怎么办?
检查清单:
验证 URL 是否正确
javascript// 确保 URL 可访问 fetch('https://your-plugin.com') .then(res => console.log('Status:', res.status)) .catch(err => console.error('Error:', err));检查 HTTPS 配置
- 生产环境必须使用 HTTPS
- 证书必须有效(不能是自签名)
- 检查证书是否过期
检查 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;检查 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;查看浏览器控制台
- 打开开发者工具
- 查看 Console 和 Network 标签
- 查找错误信息
数据保存失败怎么办?
常见原因和解决方案:
1. Token 过期
// 检查 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. 数据过大
// 检查数据大小
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. 网络问题
// 添加重试逻辑
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 | 服务器错误 | 联系技术支持 |
错误处理模板:
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') {
// 重试操作
}
}插件在某些浏览器上不工作怎么办?
浏览器兼容性检查:
// 检测浏览器特性
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 支持:
<!-- 添加 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>浏览器特定问题:
// 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 通信问题?
消息拦截器:
// 拦截所有发送的消息
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); // 使用捕获阶段消息队列监控:
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);性能问题如何排查?
性能分析工具:
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();内存泄漏检测:
// 监控内存使用
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);如何获取帮助?
自助资源:
查看文档
查看示例
搜索已知问题
- GitHub Issues: https://github.com/unityai/plugins/issues
- 搜索关键词查看是否有类似问题
联系支持:
技术支持邮箱
- support@unityai.com
- 包含以下信息:
- 插件 ID 和版本
- 错误信息和截图
- 复现步骤
- 浏览器和操作系统版本
开发者社区
- https://community.unityai.com
- 提问前先搜索是否有类似问题
- 提供完整的错误信息和代码示例
提交 Bug
- https://github.com/unityai/plugins/issues/new
- 使用 Bug 模板
- 提供最小可复现示例
调试信息收集:
// 生成调试报告
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);相关资源
获取帮助
- 技术支持:support@unityai.com
- 开发者社区:https://community.unityai.com
- 问题反馈:https://github.com/unityai/plugins/issues
