第53节:跨平台输出 - Electron/Cordova打包

概述

将Three.js应用打包为桌面端或移动端应用可以显著扩展用户群体和使用场景。本节将深入讲解如何使用Electron和Cordova将Three.js Web应用打包为跨平台的桌面和移动应用。
在这里插入图片描述

跨平台打包架构:

Three.js Web应用
桌面端打包
移动端打包
Electron
Proton Native
WebView桌面应用
Cordova
Capacitor
React Native
Windows应用
macOS应用
Linux应用
Android应用
iOS应用
混合移动应用
原生体验
应用商店分发

Electron桌面应用打包

Electron基础架构

组件职责技术实现
主进程应用生命周期、窗口管理Node.js环境
渲染进程UI渲染、Three.js运行Chromium浏览器
预加载脚本安全通信桥梁受限Node.js API
IPC通信进程间数据交换事件驱动模式

完整Electron项目配置

1. 项目结构设计
threejs-electron-app/
├── package.json
├── electron/
│   ├── main.js          # 主进程
│   ├── preload.js       # 预加载脚本
│   └── helpers.js       # 工具函数
├── src/
│   ├── index.html       # 主页面
│   ├── css/
│   ├── js/
│   │   ├── app.js       # Three.js应用
│   │   └── utils.js
│   └── assets/          # 静态资源
├── build/               # 构建输出
└── dist/               # 分发包
2. 主进程配置 (electron/main.js)
const { app, BrowserWindow, Menu, ipcMain, shell, dialog } = require('electron');
const path = require('path');
const isDev = require('electron-is-dev');

class ThreeJSApp {
    constructor() {
        this.mainWindow = null;
        this.init();
    }

    init() {
        // 应用准备就绪
        app.whenReady().then(() => {
            this.createWindow();
            this.setMenu();
            this.setIPC();
        });

        // 所有窗口关闭
        app.on('window-all-closed', () => {
            if (process.platform !== 'darwin') {
                app.quit();
            }
        });

        app.on('activate', () => {
            if (BrowserWindow.getAllWindows().length === 0) {
                this.createWindow();
            }
        });
    }

    createWindow() {
        this.mainWindow = new BrowserWindow({
            width: 1200,
            height: 800,
            minWidth: 800,
            minHeight: 600,
            webPreferences: {
                nodeIntegration: false,
                contextIsolation: true,
                enableRemoteModule: false,
                preload: path.join(__dirname, 'preload.js'),
                webSecurity: !isDev,
                allowRunningInsecureContent: isDev
            },
            icon: path.join(__dirname, '../assets/icons/icon.png'),
            show: false,
            titleBarStyle: 'default'
        });

        // 加载应用
        const startUrl = isDev 
            ? 'http://localhost:3000' 
            : `file://${path.join(__dirname, '../build/index.html')}`;
        
        this.mainWindow.loadURL(startUrl);

        // 窗口准备好后显示
        this.mainWindow.once('ready-to-show', () => {
            this.mainWindow.show();
            
            if (isDev) {
                this.mainWindow.webContents.openDevTools();
            }
        });

        // 处理外部链接
        this.mainWindow.webContents.setWindowOpenHandler(({ url }) => {
            shell.openExternal(url);
            return { action: 'deny' };
        });

        // 窗口关闭事件
        this.mainWindow.on('closed', () => {
            this.mainWindow = null;
        });
    }

    setMenu() {
        const template = [
            {
                label: '文件',
                submenu: [
                    {
                        label: '新建场景',
                        accelerator: 'CmdOrCtrl+N',
                        click: () => {
                            this.mainWindow.webContents.send('menu-new-scene');
                        }
                    },
                    {
                        label: '导出场景',
                        accelerator: 'CmdOrCtrl+E',
                        click: () => {
                            this.mainWindow.webContents.send('menu-export-scene');
                        }
                    },
                    { type: 'separator' },
                    {
                        label: '退出',
                        accelerator: process.platform === 'darwin' ? 'Cmd+Q' : 'Ctrl+Q',
                        click: () => {
                            app.quit();
                        }
                    }
                ]
            },
            {
                label: '视图',
                submenu: [
                    { role: 'reload' },
                    { role: 'forceReload' },
                    { role: 'toggleDevTools' },
                    { type: 'separator' },
                    { role: 'resetZoom' },
                    { role: 'zoomIn' },
                    { role: 'zoomOut' },
                    { type: 'separator' },
                    { role: 'togglefullscreen' }
                ]
            },
            {
                label: '帮助',
                submenu: [
                    {
                        label: '关于',
                        click: () => {
                            this.showAboutDialog();
                        }
                    }
                ]
            }
        ];

        const menu = Menu.buildFromTemplate(template);
        Menu.setApplicationMenu(menu);
    }

    setIPC() {
        // 处理文件保存
        ipcMain.handle('save-file', async (event, data) => {
            const result = await dialog.showSaveDialog(this.mainWindow, {
                defaultPath: 'scene.json',
                filters: [
                    { name: 'JSON Files', extensions: ['json'] },
                    { name: 'All Files', extensions: ['*'] }
                ]
            });

            if (!result.canceled) {
                const fs = require('fs').promises;
                try {
                    await fs.writeFile(result.filePath, JSON.stringify(data, null, 2));
                    return { success: true, path: result.filePath };
                } catch (error) {
                    return { success: false, error: error.message };
                }
            }
            return { success: false, error: '用户取消' };
        });

        // 处理文件加载
        ipcMain.handle('load-file', async (event) => {
            const result = await dialog.showOpenDialog(this.mainWindow, {
                properties: ['openFile'],
                filters: [
                    { name: 'JSON Files', extensions: ['json'] },
                    { name: 'All Files', extensions: ['*'] }
                ]
            });

            if (!result.canceled) {
                const fs = require('fs').promises;
                try {
                    const data = await fs.readFile(result.filePaths[0], 'utf8');
                    return { success: true, data: JSON.parse(data) };
                } catch (error) {
                    return { success: false, error: error.message };
                }
            }
            return { success: false, error: '用户取消' };
        });

        // 获取应用信息
        ipcMain.handle('get-app-info', () => {
            return {
                version: app.getVersion(),
                platform: process.platform,
                isDev: isDev
            };
        });
    }

    showAboutDialog() {
        dialog.showMessageBox(this.mainWindow, {
            type: 'info',
            title: '关于',
            message: 'Three.js 3D应用',
            detail: `版本: ${app.getVersion()}\nElectron: ${process.versions.electron}\nChromium: ${process.versions.chrome}\nNode.js: ${process.versions.node}`
        });
    }
}

// 启动应用
new ThreeJSApp();
3. 预加载脚本 (electron/preload.js)
const { contextBridge, ipcRenderer } = require('electron');

// 暴露安全的API给渲染进程
contextBridge.exposeInMainWorld('electronAPI', {
    // 文件操作
    saveFile: (data) => ipcRenderer.invoke('save-file', data),
    loadFile: () => ipcRenderer.invoke('load-file'),
    
    // 应用信息
    getAppInfo: () => ipcRenderer.invoke('get-app-info'),
    
    // 菜单事件
    onMenuNewScene: (callback) => ipcRenderer.on('menu-new-scene', callback),
    onMenuExportScene: (callback) => ipcRenderer.on('menu-export-scene', callback),
    
    // 移除监听器
    removeAllListeners: (channel) => ipcRenderer.removeAllListeners(channel)
});

// 在加载时初始化
window.addEventListener('DOMContentLoaded', () => {
    console.log('预加载脚本执行完成');
});
4. Three.js应用适配 (src/js/app.js)
class ThreeJSDesktopApp {
    constructor() {
        this.scene = null;
        this.camera = null;
        this.renderer = null;
        this.isElectron = false;
        
        this.init();
    }

    async init() {
        // 检查运行环境
        this.isElectron = !!(window.electronAPI);
        
        // 初始化Three.js
        await this.initThreeJS();
        
        // 设置Electron特定功能
        if (this.isElectron) {
            await this.setupElectronFeatures();
        }
        
        // 启动渲染循环
        this.animate();
    }

    async initThreeJS() {
        // 创建场景
        this.scene = new THREE.Scene();
        this.scene.background = new THREE.Color(0x1a1a2e);
        
        // 创建相机
        this.camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
        this.camera.position.set(5, 5, 5);
        this.camera.lookAt(0, 0, 0);
        
        // 创建渲染器
        this.renderer = new THREE.WebGLRenderer({ 
            antialias: true,
            powerPreference: "high-performance"
        });
        this.renderer.setSize(window.innerWidth, window.innerHeight);
        this.renderer.shadowMap.enabled = true;
        this.renderer.shadowMap.type = THREE.PCFSoftShadowMap;
        
        document.getElementById('app').appendChild(this.renderer.domElement);
        
        // 添加光源
        this.setupLighting();
        
        // 添加示例几何体
        this.createDemoScene();
        
        // 窗口大小调整
        window.addEventListener('resize', () => this.onWindowResize());
    }

    setupLighting() {
        const ambientLight = new THREE.AmbientLight(0x404040, 0.6);
        this.scene.add(ambientLight);
        
        const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
        directionalLight.position.set(10, 10, 5);
        directionalLight.castShadow = true;
        this.scene.add(directionalLight);
    }

    createDemoScene() {
        // 创建地面
        const groundGeometry = new THREE.PlaneGeometry(10, 10);
        const groundMaterial = new THREE.MeshStandardMaterial({ 
            color: 0x3a3a4a,
            roughness: 0.8,
            metalness: 0.2
        });
        const ground = new THREE.Mesh(groundGeometry, groundMaterial);
        ground.rotation.x = -Math.PI / 2;
        ground.receiveShadow = true;
        this.scene.add(ground);
        
        // 创建立方体
        const cubeGeometry = new THREE.BoxGeometry(1, 1, 1);
        const cubeMaterial = new THREE.MeshStandardMaterial({ 
            color: 0xff6b6b,
            roughness: 0.7,
            metalness: 0.3
        });
        this.cube = new THREE.Mesh(cubeGeometry, cubeMaterial);
        this.cube.position.y = 0.5;
        this.cube.castShadow = true;
        this.scene.add(this.cube);
        
        // 创建球体
        const sphereGeometry = new THREE.SphereGeometry(0.5, 16, 16);
        const sphereMaterial = new THREE.MeshStandardMaterial({ 
            color: 0x4ecdc4,
            roughness: 0.6,
            metalness: 0.4
        });
        const sphere = new THREE.Mesh(sphereGeometry, sphereMaterial);
        sphere.position.set(2, 0.5, 0);
        sphere.castShadow = true;
        this.scene.add(sphere);
    }

    async setupElectronFeatures() {
        try {
            // 获取应用信息
            const appInfo = await window.electronAPI.getAppInfo();
            this.updateAppTitle(appInfo);
            
            // 监听菜单事件
            window.electronAPI.onMenuNewScene(() => {
                this.resetScene();
            });
            
            window.electronAPI.onMenuExportScene(() => {
                this.exportScene();
            });
            
            // 添加快捷键支持
            this.setupKeyboardShortcuts();
            
        } catch (error) {
            console.error('Electron功能设置失败:', error);
        }
    }

    updateAppTitle(appInfo) {
        const title = `Three.js桌面应用 - v${appInfo.version} (${appInfo.platform})`;
        document.title = title;
        
        // 更新UI显示
        const platformInfo = document.getElementById('platform-info');
        if (platformInfo) {
            platformInfo.textContent = `运行在 ${appInfo.platform} ${appInfo.isDev ? '开发模式' : '生产模式'}`;
        }
    }

    setupKeyboardShortcuts() {
        document.addEventListener('keydown', (event) => {
            // Ctrl+S / Cmd+S 保存场景
            if ((event.ctrlKey || event.metaKey) && event.key === 's') {
                event.preventDefault();
                this.exportScene();
            }
            
            // Ctrl+O / Cmd+O 加载场景
            if ((event.ctrlKey || event.metaKey) && event.key === 'o') {
                event.preventDefault();
                this.loadScene();
            }
            
            // Ctrl+N / Cmd+N 新建场景
            if ((event.ctrlKey || event.metaKey) && event.key === 'n') {
                event.preventDefault();
                this.resetScene();
            }
        });
    }

    async exportScene() {
        if (!this.isElectron) return;
        
        const sceneData = {
            metadata: {
                version: '1.0',
                generator: 'Three.js Desktop App',
                exportedAt: new Date().toISOString()
            },
            objects: this.collectSceneObjects()
        };
        
        try {
            const result = await window.electronAPI.saveFile(sceneData);
            if (result.success) {
                this.showNotification(`场景已保存到: ${result.path}`);
            } else {
                this.showNotification(`保存失败: ${result.error}`, 'error');
            }
        } catch (error) {
            this.showNotification(`保存错误: ${error.message}`, 'error');
        }
    }

    async loadScene() {
        if (!this.isElectron) return;
        
        try {
            const result = await window.electronAPI.loadFile();
            if (result.success) {
                this.loadSceneData(result.data);
                this.showNotification('场景加载成功');
            } else {
                this.showNotification(`加载失败: ${result.error}`, 'error');
            }
        } catch (error) {
            this.showNotification(`加载错误: ${error.message}`, 'error');
        }
    }

    collectSceneObjects() {
        const objects = [];
        this.scene.traverse((object) => {
            if (object.isMesh) {
                objects.push({
                    type: object.geometry.type,
                    position: object.position.toArray(),
                    rotation: object.rotation.toArray(),
                    scale: object.scale.toArray(),
                    material: {
                        color: object.material.color.getHex(),
                        roughness: object.material.roughness,
                        metalness: object.material.metalness
                    }
                });
            }
        });
        return objects;
    }

    loadSceneData(data) {
        // 清空当前场景
        while(this.scene.children.length > 0) { 
            this.scene.remove(this.scene.children[0]); 
        }
        
        // 重新创建场景
        this.setupLighting();
        
        // 加载保存的对象
        if (data.objects) {
            data.objects.forEach(objData => {
                this.createObjectFromData(objData);
            });
        }
    }

    createObjectFromData(objData) {
        let geometry;
        
        switch (objData.type) {
            case 'BoxGeometry':
                geometry = new THREE.BoxGeometry(1, 1, 1);
                break;
            case 'SphereGeometry':
                geometry = new THREE.SphereGeometry(0.5, 16, 16);
                break;
            default:
                geometry = new THREE.BoxGeometry(1, 1, 1);
        }
        
        const material = new THREE.MeshStandardMaterial({
            color: objData.material.color,
            roughness: objData.material.roughness,
            metalness: objData.material.metalness
        });
        
        const mesh = new THREE.Mesh(geometry, material);
        mesh.position.fromArray(objData.position);
        mesh.rotation.fromArray(objData.rotation);
        mesh.scale.fromArray(objData.scale);
        mesh.castShadow = true;
        
        this.scene.add(mesh);
    }

    resetScene() {
        // 清空场景
        while(this.scene.children.length > 0) { 
            this.scene.remove(this.scene.children[0]); 
        }
        
        // 重新初始化
        this.setupLighting();
        this.createDemoScene();
        
        this.showNotification('场景已重置');
    }

    showNotification(message, type = 'info') {
        // 创建通知元素
        const notification = document.createElement('div');
        notification.className = `notification ${type}`;
        notification.textContent = message;
        
        // 添加到页面
        document.body.appendChild(notification);
        
        // 自动移除
        setTimeout(() => {
            notification.remove();
        }, 3000);
    }

    onWindowResize() {
        this.camera.aspect = window.innerWidth / window.innerHeight;
        this.camera.updateProjectionMatrix();
        this.renderer.setSize(window.innerWidth, window.innerHeight);
    }

    animate() {
        requestAnimationFrame(() => this.animate());
        
        // 旋转立方体
        if (this.cube) {
            this.cube.rotation.x += 0.01;
            this.cube.rotation.y += 0.01;
        }
        
        this.renderer.render(this.scene, this.camera);
    }
}

// 启动应用
new ThreeJSDesktopApp();
5. 构建配置 (package.json)
{
  "name": "threejs-desktop-app",
  "version": "1.0.0",
  "description": "Three.js桌面3D应用",
  "main": "electron/main.js",
  "scripts": {
    "electron": "electron .",
    "electron-dev": "ELECTRON_IS_DEV=true electron .",
    "build-web": "webpack --mode=production",
    "build-electron": "electron-builder",
    "pack": "electron-builder --dir",
    "dist": "npm run build-web && electron-builder",
    "postinstall": "electron-builder install-app-deps"
  },
  "build": {
    "appId": "com.yourcompany.threejs-app",
    "productName": "Three.js 3D应用",
    "directories": {
      "output": "dist-electron"
    },
    "files": [
      "build/**/*",
      "electron/**/*",
      "node_modules/**/*",
      "package.json"
    ],
    "mac": {
      "category": "public.app-category.graphics-design",
      "icon": "assets/icons/icon.icns",
      "target": "dmg"
    },
    "win": {
      "icon": "assets/icons/icon.ico",
      "target": "nsis"
    },
    "linux": {
      "icon": "assets/icons/icon.png",
      "target": "AppImage"
    },
    "nsis": {
      "oneClick": false,
      "allowToChangeInstallationDirectory": true
    }
  },
  "devDependencies": {
    "electron": "^22.0.0",
    "electron-builder": "^24.0.0",
    "electron-is-dev": "^2.0.0",
    "webpack": "^5.0.0",
    "webpack-cli": "^4.0.0"
  },
  "dependencies": {
    "three": "^0.150.0"
  }
}

Cordova移动应用打包

Cordova项目配置

1. 创建Cordova项目
# 安装Cordova CLI
npm install -g cordova

# 创建项目
cordova create ThreeJSMobileApp com.yourcompany.threejsapp ThreeJSMobileApp

cd ThreeJSMobileApp

# 添加平台
cordova platform add android
cordova platform add ios

# 添加插件
cordova plugin add cordova-plugin-device
cordova plugin add cordova-plugin-screen-orientation
cordova plugin add cordova-plugin-statusbar
cordova plugin add cordova-plugin-splashscreen
2. Three.js移动端适配 (www/js/app.js)
class ThreeJSMobileApp {
    constructor() {
        this.scene = null;
        this.camera = null;
        this.renderer = null;
        this.controls = null;
        
        this.touchStart = { x: 0, y: 0 };
        this.rotation = { x: 0, y: 0 };
        
        this.init();
    }

    async init() {
        // 等待设备就绪
        if (window.cordova) {
            document.addEventListener('deviceready', () => this.onDeviceReady(), false);
        } else {
            this.onDeviceReady();
        }
    }

    onDeviceReady() {
        // 设置屏幕方向
        screen.orientation.lock('landscape');
        
        // 初始化Three.js
        this.initThreeJS();
        
        // 设置触摸控制
        this.setupTouchControls();
        
        // 设置性能监控
        this.setupPerformanceMonitor();
        
        // 启动渲染循环
        this.animate();
    }

    initThreeJS() {
        // 获取视口尺寸
        const width = window.innerWidth;
        const height = window.innerHeight;
        
        // 创建场景
        this.scene = new THREE.Scene();
        this.scene.background = new THREE.Color(0x1a1a2e);
        this.scene.fog = new THREE.Fog(0x1a1a2e, 20, 100);
        
        // 创建相机 - 移动端使用更宽的视野
        this.camera = new THREE.PerspectiveCamera(70, width / height, 0.1, 1000);
        this.camera.position.set(0, 5, 10);
        
        // 创建渲染器 - 移动端优化
        this.renderer = new THREE.WebGLRenderer({ 
            antialias: true,
            powerPreference: "high-performance",
            alpha: false
        });
        this.renderer.setSize(width, height);
        this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); // 限制像素比
        this.renderer.shadowMap.enabled = true;
        this.renderer.shadowMap.type = THREE.PCFSoftShadowMap;
        
        // 添加到DOM
        document.getElementById('app').appendChild(this.renderer.domElement);
        
        // 设置光源
        this.setupLighting();
        
        // 创建场景内容
        this.createScene();
        
        // 窗口大小调整
        window.addEventListener('resize', () => this.onWindowResize(), false);
        
        // 处理返回按钮
        document.addEventListener('backbutton', () => this.onBackButton(), false);
    }

    setupLighting() {
        // 环境光
        const ambientLight = new THREE.AmbientLight(0x404040, 0.6);
        this.scene.add(ambientLight);
        
        // 方向光
        const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
        directionalLight.position.set(10, 10, 5);
        directionalLight.castShadow = true;
        directionalLight.shadow.mapSize.width = 1024;
        directionalLight.shadow.mapSize.height = 1024;
        this.scene.add(directionalLight);
    }

    createScene() {
        // 创建地面
        const groundGeometry = new THREE.PlaneGeometry(50, 50);
        const groundMaterial = new THREE.MeshStandardMaterial({ 
            color: 0x3a3a4a,
            roughness: 0.8,
            metalness: 0.2
        });
        const ground = new THREE.Mesh(groundGeometry, groundMaterial);
        ground.rotation.x = -Math.PI / 2;
        ground.receiveShadow = true;
        this.scene.add(ground);
        
        // 创建多个物体
        this.createObjects();
    }

    createObjects() {
        const geometries = [
            new THREE.BoxGeometry(1, 1, 1),
            new THREE.SphereGeometry(0.5, 16, 16),
            new THREE.ConeGeometry(0.5, 1, 16),
            new THREE.CylinderGeometry(0.5, 0.5, 1, 16),
            new THREE.TorusGeometry(0.5, 0.2, 16, 32)
        ];
        
        const colors = [0xff6b6b, 0x4ecdc4, 0x45b7d1, 0x96ceb4, 0xffe66d];
        
        geometries.forEach((geometry, index) => {
            const material = new THREE.MeshStandardMaterial({ 
                color: colors[index],
                roughness: 0.7,
                metalness: 0.3
            });
            
            const mesh = new THREE.Mesh(geometry, material);
            mesh.position.set(
                (index - 2) * 2.5,
                1,
                0
            );
            mesh.castShadow = true;
            mesh.userData = { originalY: 1, speed: 0.5 + Math.random() * 1.0 };
            
            this.scene.add(mesh);
        });
    }

    setupTouchControls() {
        const canvas = this.renderer.domElement;
        
        // 触摸开始
        canvas.addEventListener('touchstart', (event) => {
            event.preventDefault();
            const touch = event.touches[0];
            this.touchStart.x = touch.clientX;
            this.touchStart.y = touch.clientY;
        }, { passive: false });
        
        // 触摸移动 - 旋转控制
        canvas.addEventListener('touchmove', (event) => {
            event.preventDefault();
            if (event.touches.length === 1) {
                const touch = event.touches[0];
                const deltaX = touch.clientX - this.touchStart.x;
                const deltaY = touch.clientY - this.touchStart.y;
                
                this.rotation.y += deltaX * 0.01;
                this.rotation.x += deltaY * 0.01;
                
                this.touchStart.x = touch.clientX;
                this.touchStart.y = touch.clientY;
            }
        }, { passive: false });
        
        // 双指缩放
        let initialDistance = 0;
        canvas.addEventListener('touchstart', (event) => {
            if (event.touches.length === 2) {
                initialDistance = this.getTouchDistance(event.touches[0], event.touches[1]);
            }
        }, { passive: false });
        
        canvas.addEventListener('touchmove', (event) => {
            if (event.touches.length === 2) {
                event.preventDefault();
                const currentDistance = this.getTouchDistance(event.touches[0], event.touches[1]);
                const zoomDelta = (currentDistance - initialDistance) * 0.01;
                
                this.camera.position.z = THREE.MathUtils.clamp(
                    this.camera.position.z - zoomDelta,
                    5,
                    50
                );
                
                initialDistance = currentDistance;
            }
        }, { passive: false });
    }

    getTouchDistance(touch1, touch2) {
        const dx = touch1.clientX - touch2.clientX;
        const dy = touch1.clientY - touch2.clientY;
        return Math.sqrt(dx * dx + dy * dy);
    }

    setupPerformanceMonitor() {
        // 创建FPS显示
        this.stats = {
            fps: 0,
            frameCount: 0,
            lastTime: performance.now()
        };
        
        const statsElement = document.createElement('div');
        statsElement.style.cssText = `
            position: fixed;
            top: 10px;
            right: 10px;
            background: rgba(0,0,0,0.8);
            color: white;
            padding: 5px 10px;
            border-radius: 5px;
            font-family: Arial, sans-serif;
            font-size: 12px;
            z-index: 1000;
        `;
        document.body.appendChild(statsElement);
        this.statsElement = statsElement;
    }

    updatePerformanceStats() {
        this.stats.frameCount++;
        const currentTime = performance.now();
        
        if (currentTime >= this.stats.lastTime + 1000) {
            this.stats.fps = Math.round((this.stats.frameCount * 1000) / (currentTime - this.stats.lastTime));
            this.stats.frameCount = 0;
            this.stats.lastTime = currentTime;
            
            if (this.statsElement) {
                this.statsElement.textContent = `FPS: ${this.stats.fps}`;
            }
        }
    }

    onWindowResize() {
        const width = window.innerWidth;
        const height = window.innerHeight;
        
        this.camera.aspect = width / height;
        this.camera.updateProjectionMatrix();
        this.renderer.setSize(width, height);
    }

    onBackButton() {
        // 处理Android返回按钮
        if (confirm('确定要退出应用吗?')) {
            navigator.app.exitApp();
        }
    }

    animate() {
        requestAnimationFrame(() => this.animate());
        
        // 更新相机旋转
        this.camera.position.x = 10 * Math.sin(this.rotation.y);
        this.camera.position.z = 10 * Math.cos(this.rotation.y);
        this.camera.lookAt(0, 0, 0);
        
        // 物体动画
        this.scene.traverse((object) => {
            if (object.isMesh && object.userData.originalY !== undefined) {
                object.position.y = object.userData.originalY + Math.sin(Date.now() * 0.001 * object.userData.speed) * 0.5;
                object.rotation.y += 0.01 * object.userData.speed;
            }
        });
        
        // 更新性能统计
        this.updatePerformanceStats();
        
        // 渲染场景
        this.renderer.render(this.scene, this.camera);
    }
}

// 启动应用
new ThreeJSMobileApp();
3. 移动端配置 (config.xml)
<?xml version='1.0' encoding='utf-8'?>
<widget id="com.yourcompany.threejsapp" version="1.0.0" xmlns="http://www.w3.org/ns/widgets" xmlns:cdv="http://cordova.apache.org/ns/1.0">
    <name>Three.js 3D应用</name>
    <description>基于Three.js的移动端3D应用</description>
    <author email="your@email.com" href="http://yourwebsite.com">Your Name</author>
    
    <!-- 支持的方向 -->
    <preference name="Orientation" value="landscape" />
    
    <!-- 全屏显示 -->
    <preference name="Fullscreen" value="true" />
    
    <!-- 状态栏设置 -->
    <preference name="StatusBarOverlaysWebView" value="false" />
    <preference name="StatusBarBackgroundColor" value="#000000" />
    <preference name="StatusBarStyle" value="lightcontent" />
    
    <!-- 启动画面 -->
    <preference name="AutoHideSplashScreen" value="true" />
    <preference name="SplashScreenDelay" value="3000" />
    <preference name="FadeSplashScreen" value="true" />
    <preference name="FadeSplashScreenDuration" value="500" />
    
    <!-- Android配置 -->
    <platform name="android">
        <allow-intent href="market:*" />
        <preference name="AndroidInsecureFileModeEnabled" value="true" />
        
        <!-- 图标 -->
        <icon src="res/icon/android/icon-36-ldpi.png" density="ldpi" />
        <icon src="res/icon/android/icon-48-mdpi.png" density="mdpi" />
        <icon src="res/icon/android/icon-72-hdpi.png" density="hdpi" />
        <icon src="res/icon/android/icon-96-xhdpi.png" density="xhdpi" />
        
        <!-- 启动画面 -->
        <splash src="res/screen/android/splash-land-hdpi.png" density="land-hdpi"/>
        <splash src="res/screen/android/splash-land-ldpi.png" density="land-ldpi"/>
        <splash src="res/screen/android/splash-land-mdpi.png" density="land-mdpi"/>
        <splash src="res/screen/android/splash-land-xhdpi.png" density="land-xhdpi"/>
    </platform>
    
    <!-- iOS配置 -->
    <platform name="ios">
        <allow-intent href="itms:*" />
        <allow-intent href="itms-apps:*" />
        
        <!-- 图标 -->
        <icon src="res/icon/ios/icon.png" width="57" height="57" />
        <icon src="res/icon/ios/icon@2x.png" width="114" height="114" />
        <icon src="res/icon/ios/icon-40.png" width="40" height="40" />
        <icon src="res/icon/ios/icon-40@2x.png" width="80" height="80" />
        
        <!-- 启动画面 -->
        <splash src="res/screen/ios/Default-Landscape~iphone.png" width="480" height="320"/>
        <splash src="res/screen/ios/Default-Landscape@2x~iphone.png" width="960" height="640"/>
    </platform>
    
    <!-- 插件 -->
    <plugin name="cordova-plugin-whitelist" spec="1" />
    <plugin name="cordova-plugin-device" spec="2" />
    <plugin name="cordova-plugin-screen-orientation" spec="3" />
    <plugin name="cordova-plugin-statusbar" spec="2" />
    <plugin name="cordova-plugin-splashscreen" spec="6" />
    
    <!-- 访问权限 -->
    <access origin="*" />
    <allow-intent href="http://*/*" />
    <allow-intent href="https://*/*" />
</widget>

性能优化与调试

跨平台性能优化策略

平台性能挑战优化方案
Electron内存占用高代码分割、懒加载
Cordova AndroidGPU性能差异简化着色器、LOD
Cordova iOS内存限制严格纹理压缩、对象池

调试技巧

// 跨平台调试工具
class CrossPlatformDebugger {
    static log(message, data = null) {
        const timestamp = new Date().toISOString();
        const logMessage = `[${timestamp}] ${message}`;
        
        // 控制台输出
        console.log(logMessage, data || '');
        
        // Electron主进程日志
        if (window.electronAPI) {
            window.electronAPI.sendLog(logMessage, data);
        }
        
        // Cordova设备日志
        if (window.cordova && console.log) {
            console.log(logMessage);
        }
    }
    
    static performanceMark(name) {
        if (performance.mark) {
            performance.mark(`start-${name}`);
        }
    }
    
    static performanceMeasure(name) {
        if (performance.measure) {
            performance.measure(name, `start-${name}`);
            const measures = performance.getEntriesByName(name);
            const duration = measures[measures.length - 1]?.duration;
            this.log(`Performance ${name}: ${duration?.toFixed(2)}ms`);
        }
    }
    
    static memoryUsage() {
        if (window.performance && performance.memory) {
            const memory = performance.memory;
            this.log('Memory Usage', {
                used: Math.round(memory.usedJSHeapSize / 1048576) + 'MB',
                total: Math.round(memory.totalJSHeapSize / 1048576) + 'MB',
                limit: Math.round(memory.jsHeapSizeLimit / 1048576) + 'MB'
            });
        }
    }
}

通过Electron和Cordova,我们可以将Three.js Web应用打包为功能完整的桌面和移动应用,充分利用各平台的特性和能力,为用户提供原生应用般的体验。

{ "name": "E-tools", "version": "1.3.6", "description": "e-tools is an Open-Source project to support tools for desktop Mac, Windows, and Linux. ", "main": "main.js", "scripts": { "start": "electron .", "dev": "electron . --debug", "test": "mocha && standard", "pack:test": "electron-builder --dir", "pack": "set ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/ electron-builder --dir", "pack:all": "set ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/ electron-builder --platform=all", "dist": "set ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/ electron-builder", "dist:mac": "set ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/ electron-builder -m", "dist:win": "set ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/ electron-builder -w", "dist:linux": "set ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/ electron-builder -l", "package": "npm-run-all package:*", "package:mac": "electron-packager ./app --overwrite --platform=darwin --arch=x64 --out=out --icon=app/assets/app-icon/mac/app.icns --osx-sign.identity='Developer ID Application: GitHub'", "package:win": "electron-packager ./app --overwrite --platform=win32 --arch=ia32 --out=out --icon=app/assets/app-icon/win/app.ico", "package:linux": "electron-packager ./app --overwrite --platform=linux --arch=x64 --out=out", "package:sign-exe": "signcode './out/E-tools-win32-ia32/e-tools.exe' --cert ~/e-tools.p12 --prompt --name 'E-tools' --url 'http://blog.suremotoo.site/e-tools/'", "package:installer": "node ./script/installer.js", "package:sign-installer": "signcode './out/windows-installer/EtoolsSetup.exe' --cert ~/E-tools.p12 --prompt --name 'E-tools' --url 'http://blog.suremotoo.site/e-tools/'", "package:mas": "./script/mas.sh", "windows-store": "node ./script/windows-store.js", "release": "node ./script/release.js", "verify": "echo 当前ELECTRON_MIRROR=%ELECTRON_MIRROR% && echo 当前electron-builder镜像=%electron-builder-binaries_mirror%" }, "repository": { "type": "git", "url": "git+https://github.com/Suremotoo/e-tools.git" }, "keywords": [ "electron", "desktop", "tools" ], "author": "Suremotoo", "email": "suremotoo@163.com", "license": "MIT", "bugs": { "url": "https://github.com/Suremotoo/e-tools/issues" }, "homepage": "https://github.com/Suremotoo/e-tools#readme", "build": { "appId": "site.suremotoo.e-tools", "copyright": "Suremotoo", "productName": "E-tools", "dmg": {}, "mac": { "icon": "assets/app-icon/mac/app.icns" }, "win": { "icon": "assets/app-icon/win/app.ico" }, "linux": { "icon": "assets/app-icon/png/1024.png" } }, "devDependencies": { "cross-env": "^10.0.0", "electron": "^3.0.0", "electron-builder": "^26.0.12" }, "dependencies": { "body-parser": "^1.18.3", "electron-packager": "^12.0.1", "electron-store": "^2.0.0", "express": "^4.16.3", "glob": "^7.1.2", "sql-formatter": "^2.3.0", "uuid": "^3.3.2" } } 这是原始代码,我需要打包成exe桌面应用
08-24
<think>我们正在解决用户关于使用electron-builder打包生成Windows .exe文件的问题。根据之前的讨论,用户可能遇到了环境变量配置问题(如ELECTRON_MIRROR设置错误)以及打包配置问题(如未生成latest.yml文件)。下面将提供详细的配置和流程说明。 ### 一、正确配置环境变量(解决Electron镜像下载问题) 在Windows系统中,设置环境变量有两种方式: #### 方式1:临时设置(当前命令窗口有效) ```cmd set ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/ set ELECTRON_BUILDER_BINARIES_MIRROR=https://registry.npmmirror.com/-/binary/electron-builder-binaries/ npm run dist ``` #### 方式2:永久设置(推荐) 1. 打开系统环境变量设置(Win+R,输入`sysdm.cpl` → 高级 → 环境变量) 2. 在**系统变量**中新建: - 变量名:`ELECTRON_MIRROR` 变量值:`https://npmmirror.com/mirrors/electron/` - 变量名:`ELECTRON_BUILDER_BINARIES_MIRROR` 变量值:`https://registry.npmmirror.com/-/binary/electron-builder-binaries/` 3. 重启命令提示符窗口后生效。 ### 二、配置package.json(关键打包设置) #### 1. 基本配置 ```json { "name": "my-electron-app", "version": "1.0.0", "main": "main.js", "scripts": { "start": "electron .", "dist": "electron-builder --win" }, "build": { "appId": "com.example.myapp", "win": { "target": "nsis", // 生成安装包 "icon": "build/icon.ico" }, // 必须配置publish才能生成latest.yml(用于自动更新) "publish": { "provider": "generic", "url": "http://your-update-server.com/path/" // 替换为实际更新地址 } }, "devDependencies": { "electron": "^29.0.0", "electron-builder": "^24.13.3" } } ``` > **关键说明**: > - `target: "nsis"`:生成标准的Windows安装程序(.exe) > - `publish`配置:必须设置才能生成latest.yml(自动更新必需文件)[^2][^3] #### 2. 生成安装包流程 ```cmd npm install npm run dist ``` 输出文件位置:`dist/`目录下 ### 三、文件结构要求 ``` my-electron-app/ ├── package.json ├── main.js # 主进程文件 ├── preload.js # 预加载脚本(可选) ├── build/ │ └── icon.ico # 应用图标(必须256x256像素) └── src/ # 渲染进程文件 └── index.html ``` > 图标要求:Windows系统必须使用`.ico`格式,建议尺寸256x256 ### 四、常见问题解决方案 #### 问题1:打包时报错`无法下载Electron` - ✅ 检查环境变量`ELECTRON_MIRROR`是否生效 - ✅ 清除缓存:`npm cache clean --force` #### 问题2:未生成latest.yml文件 - ✅ 确认`package.json`中配置了`publish`字段[^3] -打包目标必须是安装程序(不能是`"target": "dir"`)[^2] #### 问题3:安装包图标不显示 - ✅ 确认图标路径正确且为`.ico`格式 - ✅ 使用专业转换工具(如[CloudConvert](https://cloudconvert.com/png-to-ico)) ### 五、完整打包流程示例 ```cmd # 1. 初始化项目(已存在可跳过) npm init -y # 2. 安装依赖 npm install electron --save-dev npm install electron-builder --save-dev # 3. 配置环境变量(临时) set ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/ set ELECTRON_BUILDER_BINARIES_MIRROR=https://registry.npmmirror.com/-/binary/electron-builder-binaries/ # 4. 创建必要文件 # main.js(主进程) # index.html(渲染进程) # 5. 执行打包 npm run dist ``` 成功输出: ``` • writing effective config file=dist/builder-effective-config.yaml • packaging platform=win32 arch=x64 electron=29.1.0 appOutDir=dist/win-unpacked • building target=nsis file=dist/MyApp Setup 1.0.0.exe • building block map blockMapFile=dist/MyApp Setup 1.0.0.exe.blockmap ``` 生成文件: ``` dist/ ├── MyApp Setup 1.0.0.exe # 安装程序 ├── latest.yml # 自动更新配置文件 └── builder-debug.yml # 调试配置 ``` ### 六、进阶配置(可选) #### 1. 自定义安装界面 ```json { "build": { "win": { "target": [ { "target": "nsis", "arch": ["x64"], "oneClick": false, // 启用高级安装界面 "allowToChangeInstallationDirectory": true } ] } } } ``` #### 2. 签名应用程序(避免安全警告) ```json { "build": { "win": { "certificateFile": "build/cert.pfx", "signingHashAlgorithms": ["sha256"] } } } ``` > 注意:代码签名证书需购买(约$200/年),测试阶段可跳过 --- ### 相关问题 1. 如何为Electron应用配置自动更新功能? 2. electron-builder打包时如何排除特定文件? 3. 为什么生成的安装包在部分Windows系统无法运行? 4. 如何减小electron-builder打包后的文件体积? 5. 如何为Electron应用创建桌面快捷方式和开始菜单项? > 提示:在配置过程中,如果遇到环境变量不生效的情况,请优先检查变量名拼写(注意Windows环境变量不区分大小写,但需确保正确)[^1]。科学实验中的变量控制原则提醒我们,每次只改变一个配置变量并观察结果变化,是定位问题的有效方法[^2]。 [^1]: Electron-builder打包安装包——编译篇 [^2]: electron-builder打包未生成latest.yml [^3]: electron-builder打包后没生成latest.yml文件问题</think>### Electron-builder 打包生成 Windows .exe 文件的完整配置流程 #### 一、基础环境配置(解决镜像问题) 1. **永久环境变量设置**(推荐): ```cmd :: 设置 Electron 镜像源 setx ELECTRON_MIRROR "https://npmmirror.com/mirrors/electron/" :: 设置 electron-builder 二进制镜像 setx ELECTRON_BUILDER_BINARIES_MIRROR "https://registry.npmmirror.com/-/binary/electron-builder-binaries/" ``` 重启终端后生效[^1]。 2. **临时环境变量设置**(单次有效): ```cmd set ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/ set ELECTRON_BUILDER_BINARIES_MIRROR=https://registry.npmmirror.com/-/binary/electron-builder-binaries/ npm run dist ``` #### 二、package.json 关键配置 ```json { "name": "your-app", "version": "1.0.0", "main": "main.js", "scripts": { "dist": "electron-builder --win" }, "build": { "appId": "com.example.yourapp", "win": { "target": "nsis", // 生成安装程序 "icon": "build/icon.ico" // 必须256x256像素 }, // 必须配置才能生成latest.yml "publish": { "provider": "generic", "url": "https://your-update-server.com/updates/" } } } ``` #### 三、打包执行流程 1. 安装依赖: ```cmd npm install electron-builder --save-dev ``` 2. 创建应用图标: - 将 `icon.ico` (256x256) 放在 `/build` 目录 3. 执行打包命令: ```cmd npm run dist ``` #### 四、生成文件说明 成功打包后会在 `/dist` 目录生成: ``` dist/ ├── your-app Setup 1.0.0.exe # 安装程序 ├── latest.yml # 自动更新配置文件[^2][^3] └── win-unpacked/ # 免安装目录 ``` #### 五、常见问题解决 1. **未生成 latest.yml**: - 确认 `publish` 配置已添加[^3] - 确保 `target` 不是 `"dir"`(应设为 `"nsis"` 等安装程序类型)[^2] 2. **图标不显示**: ```json "build": { "win": { "icon": "build/icon.ico" // 路径必须正确 } } ``` 3. **安装包无签名警告**: ```json "build": { "win": { "certificateFile": "path/to/cert.pfx", "signingHashAlgorithms": ["sha256"] } } ``` #### 六、完整示例配置 ```json { "name": "e-tools", "version": "1.3.6", "scripts": { "dist": "electron-builder --win --x64" }, "build": { "appId": "com.company.e-tools", "productName": "ETools", "win": { "target": "nsis", "icon": "build/icon.ico", "publisherName": "Your Company" }, "publish": { "provider": "generic", "url": "https://download.yourdomain.com/updates/" }, "nsis": { "oneClick": false, // 启用安装向导 "allowToChangeInstallationDirectory": true } } } ``` > **验证命令**:`npm run dist` 后检查 `/dist` 目录是否包含 `.exe` 和 `latest.yml` 文件[^2][^3]。 --- ### 相关问题 1. 如何为 Electron 应用配置自动更新功能? 2. electron-builder 打包时如何排除不必要的文件减小体积? 3. 为什么生成的安装包在部分 Windows 系统报安全警告? 4. 如何为 electron-builder 配置代码签名证书? 5. 除 nsis 外,electron-builder 支持哪些 Windows 安装包格式? [^1]: Electron-builder打包安装包——编译篇 [^2]: electron-builder打包未生成latest.yml [^3]: electron-builder打包后没生成latest.yml文件问题
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

二川bro

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值