内网快传不是梦

即上一篇文章,基于WebRTC 文件传输 / 共享桌面,对于文件的传输大小有一定的限制,现改为通过 Ip + Port + FileAddress,进行下载。

git地址:基于Electron实现内网快传

实现的功能及解决方式

  1. 用户在线列表 (WebSocketServer)
  2. 修改名字 (同上)
  3. 私聊/群聊 (同上)
  4. 文件拖拽发送 ( preload 递归返回文件地址树 )
  5. 首次下载需保存路径 ( dialog.showOpenDialog )
  6. 下载文件环绕动画 ( linear-gradient + 动态 processVal )
  7. 下载文件分文件夹存储 ( 下载时,默认创建接收IP存入 )
  8. 文件丢失重新下载,存在则打开 ( 递归检查丢失文件重新下载 )
  9. 历史聊天记录 ( 存储至Json,打开即重载 )
  10. 内网通过IP + 端口 + 文件地址,按流下载 ( steam + chunk + processVal )
  11. 根据文件类型展示不同Icon ( 获取后缀switchCase 配置url )

先上效果图

112df90c528c75695d91866eb178433e.png 605789b549c46f64867294188f80077d.png d253114c3a1b67d5af3f292635bcdd31.png 3adbd0dd0d31e9ad4a3d8bb74bfa0384.png

image.png

主要实现功能及技术

  1. 前端采用 Electron + Vue3
  2. 后端采用 Ws 做简单的通信,Http 做下载辅助

Electron 要点剖析 (只出重点)

main.ts (基本配置)

// 核心内容
let mainWindow: BrowserWindow;
function createWindow() {
  mainWindow = new BrowserWindow({
    icon: __dirname + "./images/speedCache.ico",
    webPreferences: {
      preload: path.join(__dirname, "preload.js"),
      contextIsolation: true,
      nodeIntegration: false,
      partition: String(+new Date()),
    },
  });

  // preload... 引用
  ipcMain.handle("get-local-ip", () => getLocalIP());
  ipcMain.handle("get-file-port", () => FILE_PORT);
  ipcMain.on("close", () => { mainWindow.close() });
  ipcMain.on("minimize", () => { mainWindow.minimize() });

  mainWindow.webContents.openDevTools(); // 调试工具

  // 测试url
  mainWindow.loadURL("http://10.0.0.155:5173/");
}

// 限制只能启动一次
app.on("second-instance", (event, commandLine, workingDirectory) => {
  if (mainWindow) {
    if (mainWindow.isMinimized()) mainWindow.restore();
    mainWindow.focus();
  }
});

if (!app.requestSingleInstanceLock()) {
  app.quit();
} else {
  app.whenReady().then(createWindow);

  app.on("window-all-closed", () => {
    if (process.platform !== "darwin") app.quit();
  });
}

preload.ts(node隔离,分离渲染层)

其中包括:获取个人Ip、端口、文件下载、文件保存路径、流式下载文件 等等....

contextBridge.exposeInMainWorld("electronAPI", {
  getLocalIP: () => ipcRenderer.invoke("get-local-ip"),
  getFilePort: () => ipcRenderer.invoke("get-file-port"),
  downloadAllFiles: (data) => ipcRenderer.invoke("downloadAllFiles", data),
  saveFileToPath: (file) => ipcRenderer.invoke("save-file-to-path", file),
  writeChunk: (data) => ipcRenderer.invoke("write-chunk", data),
});

运行个人 server.ts(固定Ip + 随机Port;用于请求下载的地址!!!)

// 随机端口
let FILE_PORT: number;
const server = http.createServer((req, res) => {
  const url: any = new URL(req.url!, `http://${req.headers.host}`);
  const action = url.pathname;

  if (action.startsWith("/download/")) {
    const encodedFilePath = action.split("/download/")[1];
    const filePath = decodeURIComponent(encodedFilePath);

    if (fs.existsSync(filePath)) {
      const stat = fs.statSync(filePath);
      const fileName = encodeURIComponent(path.basename(filePath));

      res.writeHead(200, {
        "Content-Type": "application/octet-stream",
        "Content-Length": stat.size,
        "Content-Disposition": `attachment; filename="${fileName}"`,
      });

      const readStream = fs.createReadStream(filePath); // 下载流

      readStream.on("error", (err) => {
        if (!res.headersSent) {
          res.writeHead(500, { "Content-Type": "text/plain" });
          res.end("Internal Server Error");
        } else {
          res.end();
        }
      });

      readStream.pipe(res); // 管道
      readStream.on("error", (err) => {
        console.error("File read error:", err);
        res.writeHead(500, { "Content-Type": "text/plain" });
        res.end("File read error");
      });
    } else {
      res.writeHead(404, { "Content-Type": "text/plain" });
      res.end("Not found");
    }
  }
});

server.listen(0, () => {
  const address: any = server.address();
  if (address && typeof address.port === "number") {
    FILE_PORT = address.port;
  }
});

chat-server.ts (只做文字的传输,不做下载操作)

class ChatServer {
  private wss: WebSocketServer;
  private clients: Map<string, { ws: WebSocket; name: string }> = new Map();
  constructor(usersFilePath: string, historyFilePath: string) {
    this.wss = new WebSocketServer({ port: PORT });
    this.initWebSocketServer();
  }

  private initWebSocketServer(): void {
    this.wss.on("connection", (ws: any) => {
      ws.on("message", (message: { toString: () => string }) => {
        try {
          let parsed: any;
          switch (parsed.type) {
            case "init"
            case "set-name"
            case "message"
            case "private-message"
            case "file-info"
            case "private-file-info"
            case "folder-info"
            case "private-folder-info
          }
        } 
      });

      ws.on("close", () => {
        if (id) {
          this.clients.delete(id);
          this.broadcastUserList();
        }
      });
    });
  }

前端代码就不 1 1展示了

后续希望,有A B C,设置A作为共享,文件的发送和传输A实时自动接收,即使他人文件丢失依然可以从A的文件库中获取,并且可以拓展文件的管理及UI设计的重塑....