项目背景

项目运行在Windows系统下,架构为C/S。使用iframe加载第三方链接(所以没办法从第三方链接处理,只能前端这里处理)

技术栈

列出主要的和对项目影响大的技术框架

  1. vue3
  2. electron
  3. vite
  4. typescript

下面列出我的package.json(没有藏!都在这里)
注意版本哦


"dependencies": {
    "axios": "^1.4.0",
    "changedpi": "^1.0.4",
    "echarts": "^5.4.2",
    "electron-updater": "^6.1.1",
    "element-plus": "^2.3.6",
    "pinia": "^2.1.3",
    "vue": "^3.2.25",
    "vue-router": "^4.0.15",
    "winston": "^3.14.2",
    "winston-daily-rotate-file": "^5.0.0"
  },
  "devDependencies": {
    "@types/winston": "^2.4.4",
    "@vitejs/plugin-vue": "^2.3.3",
    "@vueuse/electron": "^8.5.0",
    "electron": "^19.0.0",
    "electron-builder": "^23.0.3",
    "electron-devtools-installer": "^3.2.0",
    "rimraf": "^3.0.2",
    "typescript": "^4.5.4",
    "unplugin-auto-import": "^0.16.4",
    "unplugin-vue-components": "^0.25.0",
    "vite": "^2.9.9",
    "vite-plugin-electron": "^0.4.5",
    "vue-tsc": "^0.34.7"
  },

问题描述:

开发环境是存储cookie是没问题的,为什么呢?排查后可以发现对于electron加载前端页面的方式不同

以下是相关代码

// 打开网址(加载页面)
    let winURL;
    if (app.isPackaged) {
    // app已经被打包(生产环境)
      winURL = args.route
        ? path.join(__dirname, `../index.html/#${args.route}`)
        : path.join(__dirname, "../index.html");
      win.loadFile(winURL);
    } else {
    // app没有被打包(开发环境)
      win.webContents.openDevTools();
      winURL = args.route
        ? `http://${process.env["VITE_DEV_SERVER_HOST"]}:${process.env["VITE_DEV_SERVER_PORT"]}${args.route}?winId=${args.id}`
        : `http://${process.env["VITE_DEV_SERVER_HOST"]}:${process.env["VITE_DEV_SERVER_PORT"]}?winId=${args.id}`;
      win.loadURL(winURL);
    }

以下对于两种方式 win.loadFile(winURL)和win.loadURL(winURL)聊一聊

loadURL(url);

通过从网络加载 URL,我这里是http协议打开一个url地址(类似在浏览器输入网址)

loadURL
用途:用于从网络加载 URL。
参数:
url:要加载的 URL 字符串。
options(可选):一个对象,可以包含以下属性:
httpReferrer:HTTP 引用字符串。
userAgent:用户代理字符串。
extraHeaders:额外的 HTTP 请求头。
postData:POST 数据。
baseURLForDataURL:当加载的数据 URL 时使用的基 URL。

loadFile(filePath);

通过file协议打开一个文件目录地址(类似在本地资源管理器双击html页面)

loadFile
用途:用于从本地文件系统加载 HTML 文件。
参数:
filePath:要加载的本地 HTML 文件路径。
search(可选):查询字符串。
hash(可选):哈希片段。

在使用 loadFile 方法加载本地 HTML 文件时,处理 Cookies 的方式与使用 loadURL 加载网络 URL 有所不同。loadFile 不会自动处理 HTTP 请求和响应头中的 Cookies,因为它是从本地文件系统加载内容的,而不是通过网络请求。

解决问题思路

找到了问题,从源头我们应该把file换成url地址方式就好了。开始我也是这么想的。但是打包后改成url地址,必然要起一个sever服务,那么electron是可以使用http网络模块起vue3打包后的dist文件夹静态http服务的。

思路1(loadFile–换–>loadURL)测试失败

以下是相关代码

// serve-local-resources.ts
import * as http from "http";
import * as path from "path";
import * as fs from "fs";

let server: http.Server | undefined;

interface ServerOptions {
  port: number;
  distDir: string;
}

function startServer({ port, distDir }: ServerOptions) {
  if (server) {
    console.log("Server is already running.");
    return;
  }

  server = http.createServer((req, res) => {
    const filePath = path.join(distDir, req.url === "/" ? "index.html" : req.url || "");
    const extname = path.extname(filePath).toLowerCase();
    const mimeTypes: { [key: string]: string } = {
      ".html": "text/html",
      ".js": "text/javascript",
      ".css": "text/css",
      ".json": "application/json",
      ".png": "image/png",
      ".jpg": "image/jpg",
      ".gif": "image/gif",
      ".wav": "audio/wav",
      ".mp4": "video/mp4",
      ".woff": "application/font-woff",
      ".ttf": "application/font-ttf",
      ".eot": "application/vnd.ms-fontobject",
      ".otf": "application/font-otf",
      ".svg": "image/svg+xml",
    };

    const contentType = mimeTypes[extname] || "application/octet-stream";

    fs.readFile(filePath, (error, content) => {
      if (error) {
        if (error.code === "ENOENT") {
          fs.readFile(
            path.join(distDir, "404.html"),
            (error404, content404) => {
              res.writeHead(404, { "Content-Type": "text/html" });
              res.end(content404 || "File not found!", "utf-8");
            }
          );
        } else {
          res.writeHead(500);
          res.end(`Server Error: ${error.code}`);
        }
      } else {
        res.writeHead(200, { "Content-Type": contentType });
        res.end(content, "utf-8");
      }
    });
  });

  server.listen(port, () => {
    console.log(`Server running at http://127.0.0.1:${port}/`);
  });
  return server;
}

export default startServer;

在创建窗口前启动服务,app关闭后关掉服务

import { app, BrowserWindow } from "electron";
import { Window, serverPort } from "./window";
import path from "path";
import startServer from "./serve-local-resources";
const isDevelopment: boolean = process.env.NODE_ENV !== "production";
let server: ReturnType<typeof startServer>;
// 创建主窗口
async function createWindow() {
  // 启动HTTP服务器
  const port = serverPort;
  const distDir = path.join(__dirname, "..", "..", "dist");
  server = isDevelopment ? void 0 : startServer({ port, distDir });
  const window = new Window();
  window.listen();
  window.createWindows({ isMainWin: true });
  window.createTray();
}
const gotTheLock = app.requestSingleInstanceLock();
if (!gotTheLock) {
  app.quit();
} else {
  app.on("second-instance", (event, commandLine, workingDirectory) => {
    // Someone tried to run a second instance, we should focus our window.
  });

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

app.on("activate", () => {
  if (BrowserWindow.getAllWindows().length === 0) createWindow();
});
// 关闭服务器
app.on("will-quit", () => {
  if (server) {
    server.close(() => {
      console.log("HTTP server closed.");
    });
  }
});
app.on("ready", async () => {
  createWindow();
});

if (isDevelopment) {
  if (process.platform === "win32") {
    process.on("message", (data) => {
      if (data === "graceful-exit") {
        app.quit();
      }
    });
  } else {
    process.on("SIGTERM", () => {
      app.quit();
    });
  }
}

改造的部分不仅于此,对于窗口配置(上下文隔离打开,不启用Node集成,webSecurity 为 true,引入预加载文件)
这里我的说一下,之前项目情况,本地设备在内网运行,安全问题不在考虑范围内。所以都是安全等级最低的配置。如果改用服务的方式,前端页面要与electron完全隔离,不在可能直接调用node里的方法。

window.ts

const isOpenContextIsolation = true; //是否开启上下文隔离
......
webPreferences: {
        contextIsolation: isOpenContextIsolation, //上下文隔离
        nodeIntegration: !isOpenContextIsolation, //启用Node集成(是否完整的支持 node)
        webSecurity: true, // 确保 webSecurity 为 true
        // 引入预加载文件
        preload: path.join(__dirname, "preload.js"),
      },

preload.ts

const { contextBridge, ipcRenderer } = require("electron");
console.log("============preload......");
// 定义API接口
interface API {
  send: (channel: string, data: any) => void;
  on: (
    channel: string,
    func: (...args: any[]) => void
  ) => (() => void) | undefined;
}

// 白名单机制来验证 channel
const validChannels = [
  "readFile",
  "writeFile",
  "log",
  "deleteFile",
  "readFileDir",
  "window-reset",
];
function removePrefix(str: string) {
  const prefix = "reply-";
  if (str.startsWith(prefix)) {
    return str.replace(prefix, "");
  }
  return str;
}
// 使用 contextBridge 暴露有限的功能到渲染器进程
contextBridge.exposeInMainWorld("api", {
  send: (channel: string, data: any): void => {
    if (validChannels.includes(removePrefix(channel))) {
      ipcRenderer.send(channel, data);
    }
  },
  on: (
    channel: string,
    func: (...args: any[]) => void
  ): (() => void) | undefined => {
    if (validChannels.includes(removePrefix(channel))) {
      // @ts-ignore
      return ipcRenderer.on(channel, (event, ...args) => func(...args));
    }
  },
} as API);

之前都没有使用到preload,对了还有vite配置preload

vite.config.ts

 electron({
      main: {
        entry: "electron-main/index.ts", // 主进程文件
      },
      preload: {
        input: path.join(__dirname, "./electron-preload/index.ts"), // 预加载文件
      },
    }),
    

最后npm run build
发现会报错

ReferenceError: require is not defined at index.ts:64:29
全局搜索检查了也没发现那里我使用了 require 导致报错。
报错提示指向的位置是路由的import关键字以及import.meta.glob这两处地方,我不明白,有大佬解惑吗?(我是使用vite自动生成路由的,后面改成导入路由,倒是不报错了,但是页面是白屏。显然也是哪里出问题了)

const comModulesUser = import.meta.glob(`../views/user/**/index.vue`);
......
component: () => import("@/views/user/index.vue")

思路2(loadFile下使用cookie)测试通过

如果是使用loadFile,那么就只能在Electron上动手脚了。
思路是在请求发送后,服务器请求返回cookie时,拦截响应头并处理 Set-Cookie;
后面发送请求时,拦截请求头并添加 Cookie。进行一个欺骗模拟cookie已经设置成功效果。


    // 存储 Cookies 的对象
    const cookiesStore: Record<string, string> = {};

    // 拦截响应头并处理 Set-Cookie
    const filter = {
      urls: ["http://127.0.0.1:180800/*"],
    };

    session.defaultSession.webRequest.onHeadersReceived(
      filter,
      (details, callback) => {
        const responseHeaders = details.responseHeaders || {};
        for (let header of Object.keys(responseHeaders)) {
          if (header.toLowerCase() === "set-cookie") {
            responseHeaders[header].forEach((cookie) => {
              const cookieParts = cookie.split(";").map((part) => part.trim());
              const cookieNameValue = cookieParts[0];
              const [name, value] = cookieNameValue.split("=");
              cookiesStore[name] = value;
            });
            responseHeaders[header] = responseHeaders[header].map((cookie) =>
              cookie.replace(/Secure/gi, "").replace(/HttpOnly/gi, "")
            );
          }
        } 
        callback({ responseHeaders });
      }
    );

    // 拦截请求头并添加 Cookie
    session.defaultSession.webRequest.onBeforeSendHeaders(
      filter,
      (details, callback) => {
        let cookieString = "";
        for (const [name, value] of Object.entries(cookiesStore)) {
          cookieString += `${name}=${value}; `;
        }
        if (cookieString.length > 0) {
          details.requestHeaders.Cookie = cookieString.slice(0, -2); // Remove trailing semicolon and space
        }
        callback({ requestHeaders: details.requestHeaders });
      }
    );

最终测试思路2成功通过!

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐