Skip to content

自定义协议加载本地文件

以下两种方法都可行的(自行选择一种就行)

在 electron 中是有一个安全机制的,他不能直接访问电脑本地的文件,但是可以使用自定义协议来加载,使用 local:// 前缀(这个前缀可以自定义)

前提:如果加载很大的文件就要小心了,因为自定义协议的一次性读取可能压力很大。可以考虑在协议处理中实现流式传输,不过会比较复杂。

自定义协议

主要使用protocol模块,在主进程文件main/index.js文件的app.whenReady()中注册处理函数

js
// main.js (主进程文件)
const { app, BrowserWindow, protocol } = require("electron");
const path = require("path");
const fs = require("fs");

// 1. 在应用准备就绪前,声明协议的权限
app.whenReady().then(() => {
  protocol.registerFileProtocol("local", (request, callback) => {
    try {
      // 去除协议头,获取文件路径
      let filePath = request.url.replace("local://", "");
      // 对路径进行解码和规范化处理
      filePath = decodeURIComponent(filePath);
      filePath = path.normalize(filePath);

      // 安全检查:确保文件存在
      if (fs.existsSync(filePath)) {
        callback(filePath);
      } else {
        callback({ error: -6 }); // FILE_NOT_FOUND
      }
    } catch (error) {
      console.error("Protocol error:", error);
      callback({ error: -2 }); // FAILED
    }
  });

  // 创建窗口等后续操作
  createWindow();
});
// main.js (主进程文件)
const { app, BrowserWindow, protocol } = require("electron");
const path = require("path");
const fs = require("fs");

// 1. 在应用准备就绪前,声明协议的权限
app.whenReady().then(() => {
  protocol.registerFileProtocol("local", (request, callback) => {
    try {
      // 去除协议头,获取文件路径
      let filePath = request.url.replace("local://", "");
      // 对路径进行解码和规范化处理
      filePath = decodeURIComponent(filePath);
      filePath = path.normalize(filePath);

      // 安全检查:确保文件存在
      if (fs.existsSync(filePath)) {
        callback(filePath);
      } else {
        callback({ error: -6 }); // FILE_NOT_FOUND
      }
    } catch (error) {
      console.error("Protocol error:", error);
      callback({ error: -2 }); // FAILED
    }
  });

  // 创建窗口等后续操作
  createWindow();
});

vue 组件中注册自定义协议

vue
<script setup>
const imageURL = "local://C:/Users/luoluo/Desktop/640.png"; //window电脑路径
//如果是mac电脑路径:local:///Users/luoluo/Desktop/640.png
</script>

<template>
  <img alt="logo" class="logo" :src="imageURL" />
</template>
<script setup>
const imageURL = "local://C:/Users/luoluo/Desktop/640.png"; //window电脑路径
//如果是mac电脑路径:local:///Users/luoluo/Desktop/640.png
</script>

<template>
  <img alt="logo" class="logo" :src="imageURL" />
</template>

index.html文件的内容安全策略也要加上自定义协议local:;

html
<meta
  http-equiv="Content-Security-Policy"
  content="default-src 'self'; img-src 'self' data: local:;"
/>
<meta
  http-equiv="Content-Security-Policy"
  content="default-src 'self'; img-src 'self' data: local:;"
/>

第二种方案:handle(),这个 registerFileProtocol(已废弃)

这个 handle()方法很坑,我找了全网,没一个是对的,其实很多情况都是在net.fetch()这个有问题,

当 net.fetch 尝试访问一个 file:// URL 但因权限问题而无法访问时,就经常会报这个 400 (Bad Request) 错误。

图片展示问题:每次都是这样的问题(有时候根本不知道哪里的权限问题,无从下手)

image (1)

protocol.registerSchemesAsPrivileged([]),垃圾玩意不行的,测试过无数次,但是没用

正确的方法

js
// main.js (主进程文件)
const { app, BrowserWindow, protocol, net } = require("electron");
const path = require("path");
const fs = require("fs");
const url = require("url");

// 1. 在应用准备就绪前,声明协议的权限
app.whenReady().then(() => {
  protocol.handle("local", (request) => {
    const filePath = decodeURI(request.url.slice("local://".length));
    const absolutePath = path.isAbsolute(filePath)
      ? filePath
      : path.join(__dirname, filePath);

    return net.fetch(url.pathToFileURL(absolutePath).toString());
  });

  // 创建窗口等后续操作
  createWindow();
});
// main.js (主进程文件)
const { app, BrowserWindow, protocol, net } = require("electron");
const path = require("path");
const fs = require("fs");
const url = require("url");

// 1. 在应用准备就绪前,声明协议的权限
app.whenReady().then(() => {
  protocol.handle("local", (request) => {
    const filePath = decodeURI(request.url.slice("local://".length));
    const absolutePath = path.isAbsolute(filePath)
      ? filePath
      : path.join(__dirname, filePath);

    return net.fetch(url.pathToFileURL(absolutePath).toString());
  });

  // 创建窗口等后续操作
  createWindow();
});

文件选择器加载

结合主进程、渲染进程和 Vue 组件,动态加载用户选择的图片。

js
// main.js (主进程文件)
const { app, BrowserWindow, protocol, ipcMain } = require('electron')
const path = require('path')
const fs = require('fs')

// 1. 在应用准备就绪前,声明协议的权限
app.whenReady().then(() => {
  protocol.registerFileProtocol('local', (request, callback) => {
    try {
      // 去除协议头,获取文件路径
      let filePath = request.url.replace('local://', '')
      // 对路径进行解码和规范化处理
      filePath = decodeURIComponent(filePath)
      filePath = path.normalize(filePath)

      // 安全检查:确保文件存在
      if (fs.existsSync(filePath)) {
        callback(filePath)
      } else {
        callback({ error: -6 }) // FILE_NOT_FOUND
      }
    } catch (error) {
      console.error('Protocol error:', error)
      callback({ error: -2 }) // FAILED
    }
  })

  ipcMain.handle('select-image', async () => {
    return 'C:/Users/luoluo/Desktop/640.png' // 返回选择的图片路径
  })

  // 创建窗口等后续操作
  createWindow()
})
// main.js (主进程文件)
const { app, BrowserWindow, protocol, ipcMain } = require('electron')
const path = require('path')
const fs = require('fs')

// 1. 在应用准备就绪前,声明协议的权限
app.whenReady().then(() => {
  protocol.registerFileProtocol('local', (request, callback) => {
    try {
      // 去除协议头,获取文件路径
      let filePath = request.url.replace('local://', '')
      // 对路径进行解码和规范化处理
      filePath = decodeURIComponent(filePath)
      filePath = path.normalize(filePath)

      // 安全检查:确保文件存在
      if (fs.existsSync(filePath)) {
        callback(filePath)
      } else {
        callback({ error: -6 }) // FILE_NOT_FOUND
      }
    } catch (error) {
      console.error('Protocol error:', error)
      callback({ error: -2 }) // FAILED
    }
  })

  ipcMain.handle('select-image', async () => {
    return 'C:/Users/luoluo/Desktop/640.png' // 返回选择的图片路径
  })

  // 创建窗口等后续操作
  createWindow()
})

预加载脚本中暴露 IPC 通信方法

预加载脚本 (preload.js) 中,暴露一个安全的 selectImage 方法给渲染进程。

js
// 在预加载脚本中 (preload.js)
const { contextBridge, ipcRenderer } = require("electron");

contextBridge.exposeInMainWorld("electronAPI", {
  selectImage: () => ipcRenderer.invoke("select-image"),
});
// 在预加载脚本中 (preload.js)
const { contextBridge, ipcRenderer } = require("electron");

contextBridge.exposeInMainWorld("electronAPI", {
  selectImage: () => ipcRenderer.invoke("select-image"),
});

vue 组件

vue
<script setup>
import { ref } from "vue";
const image = ref("");
const imageShow = ref(false);

const loadImage = async () => {
  imageShow.value = true;
  const imagePath = await window.electronAPI.selectImage();
  image.value = `local://${imagePath}`;
};
</script>

<template>
  <button @click="loadImage">加载图片</button>
  <img :src="image" v-if="imageShow" alt="选择的图片" />
</template>
<script setup>
import { ref } from "vue";
const image = ref("");
const imageShow = ref(false);

const loadImage = async () => {
  imageShow.value = true;
  const imagePath = await window.electronAPI.selectImage();
  image.value = `local://${imagePath}`;
};
</script>

<template>
  <button @click="loadImage">加载图片</button>
  <img :src="image" v-if="imageShow" alt="选择的图片" />
</template>

程序员小洛文档