油猴脚本虚拟文件输入 + FileReader 封装

在油猴脚本里读取本地文件,思路是:动态创建隐藏的 <input type="file">,触发点击,再用 FileReader 读取内容。

封装函数

/**
 * 选择文件并读取内容
 * @param {Object} options
 * @param {string} [options.accept] - 文件类型限制,如 ".json,.txt"
 * @param {string} [options.readAs="text"] - text | dataURL | arrayBuffer
 * @param {string} [options.encoding="utf-8"] - 文本编码(仅 text 模式有效)
 * @returns {Promise<{file: File, content: any}>}
 */
function selectFileAndRead(options = {}) {
    const { accept = "", readAs = "text", encoding = "utf-8" } = options;

    return new Promise((resolve, reject) => {
        let input = document.getElementById("__file_input__");

        if (!input) {
            input = document.createElement("input");
            input.type = "file";
            input.id = "__file_input__";
            input.style.display = "none";
            document.body.appendChild(input);
        }

        input.accept = accept;

        input.onchange = () => {
            const file = input.files[0];
            if (!file) {
                reject(new Error("未选择文件"));
                return;
            }

            const reader = new FileReader();

            reader.onload = (e) => resolve({ file, content: e.target.result });
            reader.onerror = () => reject(new Error("读取失败"));

            switch (readAs) {
                case "dataURL":
                    reader.readAsDataURL(file);
                    break;
                case "arrayBuffer":
                    reader.readAsArrayBuffer(file);
                    break;
                default:
                    reader.readAsText(file, encoding);
            }
        };

        // 必须重置 value,否则选同一个文件不会触发 change 事件
        input.value = "";
        input.click();
    });
}

使用示例

读取文本文件

selectFileAndRead({ accept: ".txt,.json" }).then(({ file, content }) => {
    console.log(file.name, content);
});

读取 JSON 并解析

selectFileAndRead({ accept: ".json" })
    .then(({ content }) => JSON.parse(content))
    .then((data) => console.log("JSON 数据:", data))
    .catch((err) => console.error(err));

读取图片为 base64

selectFileAndRead({ accept: "image/*", readAs: "dataURL" }).then(
    ({ content }) => {
        const img = document.createElement("img");
        img.src = content;
        document.body.appendChild(img);
    }
);

async/await 版

document.addEventListener("keydown", async (e) => {
    if (e.ctrlKey && e.key === "u") {
        try {
            const { file, content } = await selectFileAndRead({ accept: ".json" });
            const config = JSON.parse(content);
            applyConfig(config);
        } catch (err) {
            console.error(err);
        }
    }
});

关键坑

浏览器安全限制input.click() 必须在用户事件(点击、键盘)的同步调用链里触发,不能在 setTimeout 或 Promise 回调里调用,否则会被拦截。

同一文件无法重复选择input.value = ""click() 前必须执行,否则选同一个文件第二次不会触发 change 事件。

中文乱码:Windows 下生成的文件有时是 GBK 编码:

selectFileAndRead({ encoding: "gbk" });

DOM 污染:函数复用同一个 __file_input__ 元素,不会重复创建,避免 DOM 堆积导致的性能问题。

在油猴脚本中的完整示例

// ==UserScript==
// @name         配置文件导入
// @namespace    http://tampermonkey.net/
// @version      1.0
// @match        https://example.com/*
// @grant        none
// ==/UserScript==

(function () {
    "use strict";

    // 插入导入按钮
    const btn = document.createElement("button");
    btn.textContent = "导入配置";
    btn.style.cssText = "position:fixed;top:10px;right:10px;z-index:9999";
    document.body.appendChild(btn);

    btn.addEventListener("click", async () => {
        const { content } = await selectFileAndRead({ accept: ".json" });
        const config = JSON.parse(content);
        console.log("已加载配置:", config);
    });

    // ... selectFileAndRead 函数定义 ...
})();