JS 动态插入 style 标签与油猴虚拟 input 读取本地文件

动态创建 style 标签

基础写法

const style = document.createElement('style');
style.textContent = `
  .my-class {
    color: red;
    font-size: 16px;
  }
`;
document.head.appendChild(style);

缺点是多次调用会重复插入,页面上堆积大量 <style> 标签影响性能。

用 id 防重复插入

function addStyle(id, css) {
  if (document.getElementById(id)) return;

  const style = document.createElement('style');
  style.id = id;
  style.textContent = css;
  document.head.appendChild(style);
}

addStyle('my-style', `
  .box {
    background: #000;
    color: #fff;
  }
`);

适合油猴脚本或浏览器插件,确保只插入一次。

可覆盖更新(推荐)

function setStyle(id, css) {
  let style = document.getElementById(id);

  if (!style) {
    style = document.createElement('style');
    style.id = id;
    document.head.appendChild(style);
  }

  style.textContent = css;
}

已存在则覆盖 textContent,不堆积。

按规则粒度插入

const style = document.createElement('style');
document.head.appendChild(style);

style.sheet.insertRule('.title { color: blue; }', 0);

适合动态拼接规则的场景,比如根据条件添加不同的选择器。

油猴中直接用 GM_addStyle

// @grant  GM_addStyle
GM_addStyle(`
  .my-class {
    color: green;
  }
`);

油猴环境推荐用内置 API,不需要手动操作 DOM。

注意事项

  • style 建议放在 head 里,放 body 部分浏览器不会立即生效
  • 频繁整块更新 textContent 会触发页面重排,CSS 内容大时注意
  • insertRule 插入的规则不会显示在开发者工具的 style 面板里(只在 Computed 里可见)

油猴虚拟 input 读取本地文件

浏览器安全限制:文件选择框必须由用户主动触发(点击/按键),不能在页面加载时自动弹出。

封装成 Promise

/**
 * @param {Object} options
 * @param {string} [options.accept]   - 文件类型限制,如 ".json,.txt"
 * @param {string} [options.readAs]   - "text" | "dataURL" | "arrayBuffer"
 * @param {string} [options.encoding] - 文本编码,默认 "utf-8"
 * @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);
      }
    };

    input.value = '';  // 清空,否则同一文件第二次选不会触发 change
    input.click();
  });
}

使用示例

读取 JSON 文件:

selectFileAndRead({ accept: '.json' })
  .then(({ content }) => JSON.parse(content))
  .then(data => console.log('解析结果:', 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);
  });

Windows 文件中文乱码:

selectFileAndRead({ accept: '.txt', encoding: 'gbk' })
  .then(({ content }) => console.log(content));

关键细节

  • 复用 input:用 id 缓存,避免多次点击在 DOM 里堆积隐藏元素
  • input.value = '':选同一个文件时 change 不会重触发,必须先清空
  • 必须用户主动触发:把 selectFileAndRead() 放在按钮点击或快捷键回调里