mixone
Version: 
MixOne is a Node scaffolding tool implemented based on Vite, used for compiling HTML5, JavasCript, Vue, React and other codes. It supports packaging Web applications with multiple HTML entry points (BS architecture) and desktop installation packages (CS a
330 lines (296 loc) • 10.8 kB
JavaScript
const path = require('path');
const chokidar = require('chokidar');
const { app, BrowserWindow, ipcMain } = require('electron');
const fs = require('fs');
const windowManager = require('./window-manager');
const { createLogger } = require('./logger');
// 保存原始的定时器函数和清除函数
const originalSetTimeout = global.setTimeout.bind(global);
const originalSetInterval = global.setInterval.bind(global);
const originalClearTimeout = global.clearTimeout.bind(global);
const originalClearInterval = global.clearInterval.bind(global);
const log = createLogger(`<brightCyan>[Main Process]<brightCyan>`)
// 用于存储所有定时器的集合
const timerManager = {
  timeouts: new Set(),
  intervals: new Set(),
  
  // 添加 setTimeout
  addTimeout(timer) {
    this.timeouts.add(timer);
  },
  
  // 添加 setInterval
  addInterval(timer) {
    this.intervals.add(timer);
  },
  
  // 清除所有定时器
  clearAll() {
    // 使用正确的清除函数
    this.timeouts.forEach(timer => {
      originalClearTimeout(timer);
    });
    this.timeouts.clear();
    
    this.intervals.forEach(timer => {
      originalClearInterval(timer);
    });
    this.intervals.clear();
    // log('All timers have been cleared');
  }
};
// 重写定时器函数,使用保存的原始函数
global.setTimeout = function wrappedSetTimeout(callback, delay, ...args) {
  const timer = originalSetTimeout(callback, delay, ...args);
  timerManager.addTimeout(timer);
  return timer;
};
global.setInterval = function wrappedSetInterval(callback, delay, ...args) {
  const timer = originalSetInterval(callback, delay, ...args);
  timerManager.addInterval(timer);
  return timer;
};
/**
 * 处理主进程文件的热重载
 * @param {string} outDir 输出目录路径
 */
function setupMainProcessHotReload(outDir) {
  log('Start setting up the hot reload of the main process...');
  
  // 添加防抖标志,避免短时间内多次触发
  let isReloading = false;
  let reloadTimeout = null;
  const mainDir = outDir;
  log('Listening directory:', mainDir);
  // 检查目录是否存在
  if (!fs.existsSync(mainDir)) {
    console.error('The listening directory does not exist:', mainDir);
    return;
  }
  // 创建文件监听器,添加更多的忽略选项
  const watcher = chokidar.watch(mainDir, {
    ignored: [
      /(^|[\/\\])\../, // 忽略隐藏文件
      '**/node_modules/**', // 忽略 node_modules
      '**/*.map', // 忽略 source map 文件
      '**/tmp/**' // 忽略临时文件
    ],
    persistent: true,
    ignoreInitial: true,
    awaitWriteFinish: {
      stabilityThreshold: 500, // 等待文件写入完成的时间
      pollInterval: 100
    },
    depth: 1 // 限制监听深度
  });
  // 处理文件变化的防抖函数
  function debounceReload(filePath, handler) {
    if (isReloading) {
      return;
    }
    isReloading = true;
    clearTimeout(reloadTimeout);
    reloadTimeout = setTimeout(() => {
      try {
        handler(filePath);
      } catch (error) {
        console.error('Reloading failed:', error);
      } finally {
        isReloading = false;
      }
    }, 300); // 300ms 防抖延迟
  }
  // 从 URL 提取窗口 ID
  function getWindowIdFromUrl(url) {
    try {
      // 匹配 http://localhost:5174/settings-window/preferences-window/index.html 格式
      // 匹配 http://localhost:5174/help-window/index.html 格式
      // 匹配 http://localhost:5174/index.html 格式
      const { pathname } = new URL(url);
      const pathSegments = pathname.split('/').filter(p => p.indexOf('.html')===-1);
      
      // 寻找包含 "-window" 的路径段
      let windowPath = pathSegments.join(path.sep);
      
      // 示例结果:
      // 1. "settings-window/preferences-window"
      // 2. "help-window"
      // 3. "" → 返回 null
      return path.sep + 'windows' + windowPath || null;
    } catch (error) {
      console.error('Failed to parse the window URL:', error);
      return null;
    }
  }
  let mainWindow = null;
  const windows = new Set();
  // 保存所有已加载的模块缓存
  const cachedModules = new Map();
  // 移除所有 IPC 处理程序
  function removeIpcHandlers() {
    try {
      // console.log('Prepare to remove the IPC handler');
      // 移除 call-main-fn 处理程序
      ipcMain.removeHandler('call-main-fn');
      // console.log('The IPC handler has been removed');
    } catch (error) {
      console.error('The removal of the IPC handler failed:', error);
    }
  }
  // 清除模块缓存
  function clearModuleCache(modulePath) {
    try {
      // console.log('Prepare to clear the module cache:', modulePath);
      // 删除 require 缓存
      delete require.cache[require.resolve(modulePath)];
      // console.log('The module cache has been cleared:', modulePath);
    } catch (error) {
      console.error('Failed to clear the module cache:', error);
    }
  }
  // 重新加载模块
  function reloadModule(modulePath, realChangePath = []) {
    try {
      // console.log('Prepare to reload the module:', modulePath);
      // 先移除旧的 IPC 处理程序
      removeIpcHandlers();
      // 清除模块缓存
      clearModuleCache(modulePath);
      realChangePath.forEach((filePath) => {
        clearModuleCache(filePath);
      });
      // 重新加载模块
      const newModule = require(modulePath);
      cachedModules.set(modulePath, newModule);
      // console.log('The module has been reloaded:', modulePath);
      return newModule;
    } catch (error) {
      console.error('The reloading of the module failed:', error);
      return null;
    }
  }
  // 保存窗口状态
  function saveWindowStates() {
    let windowStateStr = {};
    BrowserWindow.getAllWindows().forEach(win => {
      let save_win  = windowManager._getWindowInfo(win.id);
      const url = win.webContents.getURL();
      const windowPath = getWindowIdFromUrl(url);
      //经过考虑modal窗口不用恢复,因为一旦恢复了,父窗口实际是不能控制的。反而成了大问题。
      if(!save_win.windowOptions.modal){
        let item = {
          windowPath,
          // 基本属性
          bounds: win.getBounds(),
          url: win.webContents.getURL(),
          // 窗口状态
          isMaximized: win.isMaximized(),
          isMinimized: win.isMinimized(),
          isFullScreen: win.isFullScreen(),
          // 窗口配置
          resizable: win.isResizable(),
          movable: win.isMovable(),
          minimizable: win.isMinimizable(),
          maximizable: win.isMaximizable(),
          closable: win.isClosable(),
          modal:win.isModal(),
          // 其他重要配置
          alwaysOnTop: win.isAlwaysOnTop(),
          autoHideMenuBar: win.autoHideMenuBar,
          windowOptions:save_win.windowOptions
        }
        // 处理子窗口的配置
        
        let childrenWinIds = Object.keys(save_win.children || {});
        if(childrenWinIds.length>0){
          item.childrenWindowOptions = childrenWinIds.map(child_win_id=>{
            return windowManager._getWindowInfo(Number(child_win_id));
          })
        }
        windowStateStr[win.id] = JSON.stringify(item);
      } else {
        console.log(' window '+win.id+' is a modal window and does not save state ')
      }
      // 获取完整的窗口配置
    });
    
    // 将窗口状态写入临时文件
    const statesPath = path.join(app.getPath('temp'), 'window-states.txt');
    fs.writeFileSync(statesPath, Object.values(windowStateStr).join('------'));
    console.log('The window status has been saved to a temporary file:', statesPath);
    return statesPath;
  }
  // 处理 main.js 的变化
  function handleMainJsChange() {
    // 保存窗口状态
    saveWindowStates();
    // 重启应用
    console.log('Restart the application and get ready...');
  }
  // 处理其他 JS 文件的变化
  function handleOtherJsChange(filePath) {
    // console.log('File changes have been detected:', filePath);
    
    // 清除所有定时器
    timerManager.clearAll();
    let fnFilePath = path.join(path.dirname(filePath), 'fn.js');//因为以.fn.js的文件变化也需要在fn.js中重载。所有不能读取变化的.fn.js文件
    // 重新加载模块
    const reloadedModule = reloadModule(fnFilePath,[filePath]);//变化的文件也要传入、为了清楚缓存,然后fn文件内部重新执行的时候就不会缓存了。
    
    if (reloadedModule) {
      // 如果模块有 hot reload 处理函数,则调用它
      if (typeof reloadedModule.onHotReload === 'function') {
        console.log('Call the onHotReload function of the module');
        reloadedModule.onHotReload();
      }
      // 如果模块导出了新的主进程函数,需要重新注册
      if (typeof reloadedModule.registerMainFunctions === 'function') {
        // console.log('Re-register the main process function');
        reloadedModule.registerMainFunctions();
      }
      
      log('Module hot reload completed:', filePath);
    }
  }
  function fnReload(filePath) {
    const normalizedPath = path.normalize(filePath);
    // 使用防抖处理文件变化
    if (normalizedPath.endsWith('main.js')) {
      console.log('main.js has changed and needs to be restarted to take effect...');
      handleMainJsChange()
    } else if (normalizedPath.endsWith('fn.js')) {
      // console.log('Changes to the fn.js file, perform special processing...');
      debounceReload(normalizedPath, handleOtherJsChange);
    } else {
      handleMainJsChange()
      console.log('The main directory has changed and needs to be restarted to take effect...');
    }
  }
  // 监听文件变化
  watcher.on('all', (event,filePath) => {
    console.log(`Detected file${event}:`, filePath);
    if (['add', 'change', 'unlink'].includes(event)) {
      fnReload(filePath)
    }
  });
  // 监听错误,添加更多错误处理
  watcher.on('error', (error) => {
    console.error('File listener error:', error);
    if (error.stack) {
      console.error('Error stack:', error.stack);
    }
    
    // 尝试重新启动监听器
    try {
      watcher.close();
      setTimeout(() => {
        setupMainProcessHotReload(outDir);
      }, 1000);
    } catch (e) {
      console.error('Restarting the listener failed:', e);
    }
  });
  // 监听就绪事件
  watcher.on('ready', () => {
    console.log('The file listener is ready');
  });
  console.log('The hot reload Settings for the main process have been completed');
  // 确保在应用退出时关闭监听器
  app.on('before-quit', () => {
    watcher.close();
    clearTimeout(reloadTimeout);
  });
  return watcher;
}
module.exports = {
  setupMainProcessHotReload,
  timerManager // 导出定时器管理器以便其他模块使用
};