我花3天"抄"了Dark Reader核心算法,做了个护眼插件(10个内测,3个问价)

49 阅读8分钟

"15809行 → 500行,砍掉95%复杂度,专攻小白市场 | 技术不难,关键是看到了100万+小红书女性用户的痛点"

Gemini_Generated_Image_oiyj4woiyj4woiyj.png

💰 先说结论:这是一门生意,不是作业

上周我把这个插件发给了10个朋友内测,有3个人问我:"这个能卖多少钱?我想买。"

这让我意识到:Dark Reader虽然强大,但它服务的是工程师,而不是占浏览器用户80%的普通人。

这就是商机。


🎯 起点:一个有价值的的吐槽

我女朋友刷小红书时随口一句话,让我看到了机会:

"Dark Reader这破插件,7个滑动条我一个都看不懂,我就想要个粉粉嫩嫩的页面,为什么要我学RGB?"

我当时就想:这TM不就是产品定位的经典案例吗?

  • Dark Reader月活500万+,但转化率极低
  • 普通用户(尤其是女性用户)需求明确:好看、护眼、傻瓜式
  • 小红书/抖音上一堆"护眼设置教程"视频,播放量百万级

市场需求 ✅,竞品痛点 ✅,技术门槛?我去看看Dark Reader源码。


� 技术突破:我是怎么"偷"走核心算法的

Dark Reader是开源的,这是好消息。但15809行代码,我TMD从哪看起?

第一步:定位核心价值

我花了4小时,用grep和代码搜索,找到了Dark Reader的灵魂

// 位置:/inject/index.js 第 1540-1700 行
// 这160行代码,就是让500万用户愿意装这个插件的原因

function multiplyMatrices(m1, m2) {
  // 5x5颜色矩阵变换 - 计算机图形学经典算法
}

const Matrix = {
  invertNHue()  // Dark Reader的精髓:反色+色相旋转
}

关键洞察

Dark Reader的核心不是UI,不是配置管理,而是颜色矩阵变换算法

这个算法解决了一个技术难题:

  • 简单的filter: invert(100%)会让红色变青色(难看)
  • Dark Reader用invert(100%) + hue-rotate(180deg)保持颜色和谐(舒服)

这160行代码,值1000万。

第二步:砍掉90%的代码

我做了一件"反工程师"的事:我不要灵活性,我只要5个预设。

// Dark Reader:让用户自己调参数(工程师思维)
brightness: ???  // 用户:我TM怎么知道?
contrast: ???
sepia: ???

// 我的插件:直接给答案(产品思维)
const PRESETS = {
  sakura: {
    brightness: 100,
    contrast: 95,
    sepia: 10,  // 这是我测试了100遍后的黄金配比
    targetColor: '#ffe8f0'  // 小红书最火的樱花粉色号
  }
}

结果

  • Dark Reader:15809行,7个滑动条,学习成本30分钟
  • 我的插件:500行,10个按钮,学习成本3秒

这就是产品化的本质:用技术为用户做决策。


💡 商业化思路:为什么这玩意能赚钱

市场定位

维度Dark Reader我的插件市场规模
目标用户程序员、技术宅小红书/抖音女性用户10倍差距
客单价免费(捐赠模式)¥9.9/月 或 ¥49/永久可量化收益
获客成本GitHub/技术社区小红书/B站教程植入流量更便宜
付费意愿低(工具属性)高(颜值+健康)女性用户付费率3倍

变现路径

阶段1:免费版打爆小红书

  • 发10个"护眼神器"笔记,植入下载链接
  • 预计曝光50万+,转化下载5000+

阶段2:增值服务

  • 基础版:免费(10个预设主题)
  • Pro版:¥9.9/月(自定义主题+云同步)
  • 终身版:¥49(买断制)

阶段3:B端合作

  • 与护眼灯品牌联名(流量置换)
  • 企业定制版(公司统一配置护眼主题)

保守估算

  • 月活用户:10万(小红书用户基数大)
  • 付费转化率:3%(女性用户付费意愿强)
  • 月收入:3万+

这不是情怀项目,这是可验证的商业模式。


📚 技术细节(给工程师看的)

如果你是技术合伙人候选人,这部分给你看我的技术能力:

核心创新:预设配置代替参数调节

Dark Reader 的困境

// 用户需要自己调整:
{
  brightness: ???,  // 用户懵了
  contrast: ???,    // 不知道调多少
  sepia: ???,       // 这是什么?
  grayscale: ???    // 完全不懂
}

我的解决方案

// 提供科学调试好的预设 (共10个主题)
const THEME_CONFIGS = {
  sakura: {
    name: '🌸 樱花粉',
    filter: {
      brightness: 100,  // 保持正常亮度
      contrast: 95,     // 降低 5% 让眼睛舒适
      sepia: 10,        // 10% 复古色 (黄金比例)
    },
    lightSchemeBackgroundColor: '#ffe8f0',  // 真正的樱花粉!
    lightSchemeTextColor: '#2d1a23'         // 深棕色文本
  },
  
  matcha: {
    name: '🍵 抹茶绿',
    filter: {
      brightness: 100,
      contrast: 92,
      sepia: 8,
    },
    lightSchemeBackgroundColor: '#e8f5e8',  // 豆沙绿
    lightSchemeTextColor: '#1a2e1a'
  },
  
  // ... 还有8个主题:奶茶色、深邃黑、薄雾屏
  // 薰衣草、蜜桃橙、海洋蓝、晚霞金、薄荷青
}

调试过程 (我替用户做了):

  1. 测试 brightness: 90-110 (每 1% 一组)
  2. 测试 contrast: 80-100 (每 2% 一组)
  3. 测试 sepia: 0-30 (每 2% 一组)
  4. 在不同网站验证 (百度、知乎、小红书...)
  5. 选出最舒适的组合

🎨 深入技术:Dark Reader 的核心算法揭秘

1. 暗黑模式的精髓

很多人以为暗黑模式就是 filter: invert(100%),但这会导致颜色错乱

/* ❌ 简单反色的问题 */
filter: invert(100%);

问题:
- 红色 → 青色 ❌
- 蓝色 → 黄色 ❌
- 绿色 → 品红色 ❌

Dark Reader 的解决方案

/* ✅ 反色 + 色相旋转 */
filter: invert(100%) hue-rotate(180deg);

效果:
- 颜色保持相对和谐 ✅
- 视觉效果更舒适 ✅

为什么要加 hue-rotate(180deg)?

因为 invert(100%) 会反转 RGB 值,但色相也会错乱。通过再旋转 180 度,可以让颜色回到视觉上和谐的状态。

2. 图片双重反转技巧

暗黑模式下,图片也会被反色,怎么办?

/* 页面反色 */
html {
  filter: invert(100%) hue-rotate(180deg);
}

/* 图片再反转一次,恢复正常 */
img, video, picture {
  filter: invert(100%) hue-rotate(180deg) !important;
}

数学原理: 两次反色 = 恢复原样 (负负得正)

3. CSS Filter 组合的顺序很重要

function getCSSFilterValue(config) {
  const filters = [];
  
  // 顺序必须严格遵守!
  if (config.mode === 'dark') {
    filters.push('invert(100%) hue-rotate(180deg)');
  }
  if (config.brightness !== 100) {
    filters.push(`brightness(${config.brightness}%)`);
  }
  if (config.contrast !== 100) {
    filters.push(`contrast(${config.contrast}%)`);
  }
  if (config.sepia > 0) {
    filters.push(`sepia(${config.sepia}%)`);
  }
  
  return filters.join(' ');
}

为什么顺序重要?

CSS Filter 是从左到右依次应用的,不同的顺序会产生完全不同的效果。


🚧 踩过的坑:从失败到成功

坑 1: 简化版的樱花粉不够粉

问题: 用户反馈 "这不是樱花粉,只是微黄色"

原因分析:

// ❌ 我一开始只用了 CSS Filter
filter: sepia(8%);

// 白色 + sepia(8%) ≈ #FFF5F0 (浅米色)
// 不是我们想要的 #ffe8f0 (樱花粉)

解决方案: 学习 Dark Reader 的目标背景色功能

// ✅ 升级后: Filter + 精确背景色
const sakura = {
  filter: {
    sepia: 10  // 暖色调
  },
  lightSchemeBackgroundColor: '#ffe8f0',  // 直接指定目标色!
  lightSchemeTextColor: '#2d1a23'         // 自动调整文本对比度
}

效果对比:

方案白色页面效果视觉感受
旧版 (sepia only)#FFF5F0浅米色,不明显
新版 (target color)#ffe8f0真正的樱花粉 ✅

坑 2: Manifest V3 的权限问题

Chrome 从 Manifest V2 升级到 V3,权限模型完全变了。

错误提示:

Error: Uncaught (in promise) Error: 
Could not establish connection. Receiving end does not exist.

解决方案: 更新 manifest.json

{
  "manifest_version": 3,
  "name": "护眼插件",
  "version": "1.0.0",
  "permissions": [
    "storage",      // 保存用户配置
    "activeTab"     // 访问当前标签页
  ],
  "background": {
    "service_worker": "background/background.js"  // V3 使用 service_worker
  },
  "content_scripts": [
    {
      "matches": ["<all_urls>"],
      "js": ["content/content.js"],
      "run_at": "document_start",  // 尽早注入
      "all_frames": true            // 包括 iframe
    }
  ]
}

坑 3: Content Scripts 的自动加载

最佳实践: 使用 manifest 中的 content_scripts 配置,而非动态注入

"content_scripts": [
  {
    "matches": ["<all_urls>"],
    "js": ["content/content.js"],
    "run_at": "document_start"  // 关键:在页面加载前注入
  }
]

优势:

  • ✅ 自动注入,无需手动管理
  • ✅ 页面刷新后自动生效
  • ✅ 避免权限问题

📦 最终架构:500 行的极简实现

文件结构

SimpleReader/
├── manifest.json          # Chrome 插件配置 (60 行)
├── popup/
│   ├── popup.html        # 主界面 (100 行)
│   ├── popup.css         # 渐变样式 (150 行)
│   └── popup.js          # 交互逻辑 (150 行)
├── content/
│   └── content.js        # Dark Reader 核心算法精简版 (250 行)
├── background/
│   └── background.js     # 后台服务 (50 行)
└── icons/                # 图标资源

代码量对比:

项目代码行数功能完成度
Dark Reader15,809 行100% (专业级)
我的插件500 行80% (核心功能)

保留的功能:

  • ✅ 核心颜色算法 (100%)
  • ✅ 暗黑模式算法 (100%)
  • ✅ 图片双重反转 (100%)
  • ✅ 记住每个网站的选择 (100%)

砍掉的功能:

  • ❌ 7 个滑动条 → 10 个预设按钮
  • ❌ 高级设置面板 → 一键切换
  • ❌ 自定义颜色 → 科学预设(10套精选主题)
  • ❌ 动态分析引擎 → 静态配置

🎨 UI 设计:让技术有温度

设计理念

Dark Reader 的 UI: 工程师风格,功能至上

我的 UI: 小红书风格,颜值即正义

渐变按钮效果

.theme-button.sakura {
  background: linear-gradient(135deg, 
    #FFE8F0 0%,      /* 浅樱花粉 */
    #FFD5E5 50%,     /* 中樱花粉 */
    #FFC0D9 100%     /* 深樱花粉 */
  );
  box-shadow: 0 4px 15px rgba(255, 182, 193, 0.4);
}

.theme-button.sakura:hover {
  transform: translateY(-2px);  /* 上浮效果 */
  box-shadow: 0 6px 20px rgba(255, 182, 193, 0.6);
}

效果: 鼠标悬停时按钮会"浮起来",让操作有反馈感。

当前主题指示

// 通过特殊样式显示当前激活的主题
if (isActive) {
  button.classList.add('active');
  button.innerHTML += ' <span class="checkmark">✓</span>';
}

🎯 核心代码实现

1. 主题配置 (popup.js)

const THEME_CONFIGS = {
  sakura: {
    name: '樱花粉',
    filter: {
      mode: 'light',
      brightness: 100,
      contrast: 95,
      sepia: 10,
      grayscale: 0
    },
    lightSchemeBackgroundColor: '#ffe8f0',
    lightSchemeTextColor: '#2d1a23',
    overlayGradient: 'linear-gradient(rgba(255, 230, 240, 0.08), rgba(255, 230, 240, 0.08))',
  },
  // ...其他主题
};

2. 应用主题逻辑

async function applyTheme(themeName) {
  const [tab] = await chrome.tabs.query({
    active: true,
    currentWindow: true
  });
  const url = new URL(tab.url).hostname;
  
  // 1. 保存配置到 Chrome Storage
  await chrome.storage.local.set({ [url]: themeName });
  
  // 2. 发送消息给 content script
  try {
    await chrome.tabs.sendMessage(tab.id, {
      action: 'applyTheme',
      theme: THEME_CONFIGS[themeName]
    });
  } catch (err) {
    console.warn('Failed to send message:', err);
  }
  
  // 3. 更新 UI 状态
  updateUI(themeName);
  
  // 4. 更新扩展图标颜色
  updateExtensionIcon(themeName, true);
}

3. 页面样式注入 (content.js)

function applyThemeToPage(themeConfig) {
  // 1. 移除旧样式
  if (currentStyle) {
    currentStyle.remove();
    currentStyle = null;
  }
  
  // 2. 设置页面标记
  document.documentElement.setAttribute('data-reader-mode', 'active');
  document.documentElement.setAttribute('data-reader-scheme', themeConfig.name);
  
  // 3. 创建新样式
  currentStyle = document.createElement('style');
  currentStyle.id = 'reader-theme';
  
  // 4. 生成 CSS Filter
  const filterValue = getCSSFilterValue(themeConfig.filter);
  
  // 5. 判断是否使用高级模式(目标背景色)
  const useAdvancedMode = themeConfig.lightSchemeBackgroundColor && 
                          themeConfig.lightSchemeTextColor;
  
  // 6. 构建完整 CSS
  const css = `
    /* 基于 Dark Reader 核心算法 */
    ${useAdvancedMode ? `
    /* 高级模式:精确目标配色 */
    html {
      ${filterValue ? `filter: ${filterValue} !important;` : ''}
    }
    
    body, html {
      background-color: ${themeConfig.lightSchemeBackgroundColor} !important;
    }
    ` : `
    /* 简单模式:仅使用滤镜 */
    html {
      ${filterValue ? `filter: ${filterValue} !important;` : ''}
      ${themeConfig.backgroundColor ? 
        `background-color: ${themeConfig.backgroundColor} !important;` : ''}
    }
    `}
    
    ${themeConfig.filter.mode === 'dark' ? `
    /* 暗黑模式:图片双重反转 */
    img, picture, video, canvas, svg {
      filter: invert(100%) hue-rotate(180deg) !important;
    }
    ` : ''}
    
    ${themeConfig.overlayGradient ? `
    /* 渐变遮罩层 */
    body::before {
      content: "";
      position: fixed;
      top: 0; left: 0; width: 100%; height: 100%;
      background: ${themeConfig.overlayGradient};
      pointer-events: none;
      z-index: 999999;
    }
    ` : ''}
    
    /* 平滑过渡 */
    html, body {
      transition: filter 0.3s ease, background-color 0.3s ease !important;
    }
  `;
  
  currentStyle.textContent = css;
  document.head.appendChild(currentStyle);
}

4. Filter 生成器

function getCSSFilterValue(filter) {
  const filters = [];
  
  // 严格按照 Dark Reader 的顺序
  if (filter.mode === 'dark') {
    filters.push('invert(100%) hue-rotate(180deg)');
  }
  if (filter.brightness !== 100) {
    filters.push(`brightness(${filter.brightness}%)`);
  }
  if (filter.contrast !== 100) {
    filters.push(`contrast(${filter.contrast}%)`);
  }
  if (filter.sepia > 0) {
    filters.push(`sepia(${filter.sepia}%)`);
  }
  if (filter.grayscale > 0) {
    filters.push(`grayscale(${filter.grayscale}%)`);
  }
  
  return filters.join(' ') || 'none';
}

5. 自动加载保存的主题

// Content Script 初始化时自动加载
(async function initTheme() {
  try {
    const hostname = location.hostname;
    
    // 1. 从 Storage 读取保存的主题
    const result = await chrome.storage.local.get([hostname]);
    const savedTheme = result[hostname];
    
    if (savedTheme && savedTheme !== 'reset') {
      // 2. 请求完整的主题配置
      chrome.runtime.sendMessage({
        action: 'getThemeConfig',
        themeName: savedTheme
      }, (response) => {
        if (response && response.theme) {
          // 3. 应用主题
          applyThemeToPage(response.theme);
        }
      });
    }
  } catch (err) {
    console.warn('Failed to load saved theme:', err);
  }
})();

特点:

  • ✅ 页面加载时自动应用
  • ✅ 记住每个网站的选择
  • ✅ 刷新页面立即生效

📊 成果与数据

用户反馈 (内测)

小美 (学生): "终于不用调参数了!点一下就有粉粉的页面,太治愈了!"

小李 (程序员女友): "晚上看小说再也不刺眼了,抹茶绿主题真的很护眼!"


🤔 技术反思

1. 开源的力量

Dark Reader 是开源项目,这让我可以:

  • 学习顶级的颜色算法实现
  • 理解 Chrome Extension API 的最佳实践
  • 站在巨人肩膀上快速迭代

感谢开源! 没有 Dark Reader,就没有这个项目。

2. 极简设计的哲学

"Perfection is achieved, not when there is nothing more to add, but when there is nothing left to take away." — Antoine de Saint-Exupéry

删减功能 ≠ 降低质量,而是:

  • 明确目标用户 (小白用户 vs 专业用户)
  • 保留核心价值 (护眼效果)
  • 砍掉决策成本 (7 个滑动条 → 10 个精选主题)

3. 预设配置的价值

用户不需要"灵活性",他们需要的是"答案"。

就像:

  • Instagram 的滤镜 (不是让你调 RGB)
  • 美图秀秀的一键美颜 (不是 Photoshop)
  • iPhone 的拍照模式 (不是专业相机)

好的设计 = 替用户做决策


🚀 未来规划

Phase 1: 核心功能完善 ✅

  • 10 个精选预设主题
  • 记住每个网站的选择
  • Dark Reader 核心算法集成

Phase 2: 体验优化 (进行中)

  • 快捷键支持 (如 Ctrl+Shift+P 切换主题)
  • 网站黑白名单 (部分网站自动跳过)
  • 平滑过渡动画

Phase 3: 社区扩展

  • 用户自定义主题
  • 主题市场 (类似 VSCode 主题)
  • 导出/导入配置

💬 最后:我不需要"合作",我需要"合伙人"

写这篇文章不是为了炫技,是为了找到志同道合的人。

留言/私信,我看到就回。