如果你经常上网,一定对广告感到厌烦。无论是视频前的60秒等待,还是文章中间突然弹出的弹窗,甚至是右下角不断闪烁的悬浮图标,都在不断打断你的注意力,消耗你的耐心。传统的广告拦截器(Adblocker)确实能解决“看得到”的问题,但它们通常只是简单粗暴地将广告元素从网页上抹去,留下一片空白。你有没有想过,这些被清空的时间和空间,能不能用来做点更有意义的事?
今天要介绍的项目Palimpscape,就给出了一个让人眼前一亮的答案。它不仅仅是一个广告拦截器,更是一个“内容转换器”。它的核心逻辑是:与其让广告位变成空白,不如用有价值的内容——比如英语词汇——来填充它。你在浏览网页时,原本显示广告的地方,会变成一个个精心挑选的单词或短语及其释义。
这听起来像是一个小巧思,但其背后的产品哲学值得深究。它解决的不仅仅是“去除干扰”的问题,更是“注意力回收”和“碎片时间利用”的问题。对于开发者、学生、或者任何希望提升英语能力的读者来说,这相当于将被迫浪费的浏览时间,转化为了主动学习的契机。
本文将带你深入解析 Palimpscape。我们不仅会探讨它的工作原理、如何部署,还会通过实际代码示例,教你如何理解甚至定制它的词汇替换规则。更重要的是,我们会分析这种“积极拦截”模式背后的技术实现、潜在挑战,以及它给浏览器扩展开发带来的新思路。
1. 这篇文章真正要解决的问题
在深入代码之前,我们首先要厘清 Palimpscape 究竟解决了什么层面的问题。这绝不是一个“玩具项目”,它触及了现代网页浏览体验中的几个核心痛点:
1. 注意力经济的负外部性:当前网页的商业模式严重依赖广告,但广告的过度投放导致了用户体验的急剧下降。用户安装了广告拦截器,本质上是与网站进行一场“军备竞赛”。Palimpscape 提供了一种“建设性对抗”的思路:我不破坏你的页面结构,但我改变你投放内容的性质,将商业信息替换为教育内容。
2. 碎片化时间的价值挖掘:我们每天都会在等视频广告、快速滑动跳过信息流广告上花费不少时间。这些时间碎片难以系统利用。Palimpscape 的巧妙之处在于,它将这些不可避免的“中断时刻”变成了潜在的“学习时刻”,无需用户主动改变行为习惯。
3. 传统广告拦截器的局限性:传统拦截器(如 uBlock Origin)功能强大,但其交互是“无声”的。用户只知道广告没了,但不知道发生了什么。Palimpscape 则提供了“显性反馈”,让用户明确感知到拦截行为并获得了即时收益(学到一个新词),这增强了工具的价值感和用户的正向心理体验。
4. 开发者学习与启发:对于前端开发者和浏览器扩展开发者而言,Palimpscape 是一个绝佳的案例。它展示了如何通过操作 DOM(文档对象模型)和监听网络请求,来实现精细化的内容重写策略,而不仅仅是简单的屏蔽。
因此,本文的目标读者是:
- 希望提升浏览体验和学习效率的普通用户。
- 对浏览器扩展开发感兴趣,想学习内容脚本(Content Script)和网络请求拦截技术的前端开发者。
- 关注“人性化技术设计”的产品经理或设计师。
接下来,我们将从原理到实践,完整拆解这个项目。
2. 基础概念与核心原理
要理解 Palimpscape,需要先掌握几个关键概念:
广告拦截器 (Adblocker) 是如何工作的?传统广告拦截器主要通过两种方式:
- 基于规则的过滤:维护一个庞大的规则列表(如 EasyList),这些规则描述了广告元素的特征(如
div[id*="ad-"],script[src*="adsystem"])。扩展程序将这些规则注入页面,匹配到的元素会被隐藏或移除。 - 网络请求拦截:监听网页发起的所有网络请求,如果请求的 URL 匹配广告服务器域名(如
doubleclick.net,googleads.com),则阻止该请求,资源无法加载,广告自然无法显示。
Palimpscape 的创新点:内容替换而非简单屏蔽Palimpscape 同样运用了上述技术来识别广告。但关键区别在于后续操作:
- 传统方式:
element.remove()或element.style.display = 'none'。 - Palimpscape 方式:
element.innerHTML = ‘<div class=“vocab-word”>...<div>’。
它把广告元素变成了一个展示词汇的“容器”。为了实现这一点,它需要解决几个问题:
- 识别准确性:如何精准定位广告元素,避免误伤正常内容?
- 替换内容源:词汇库从哪里来?如何保证词汇的质量和适用性?
- 样式与布局:替换后的词汇卡片如何保持页面布局基本稳定,甚至看起来更美观?
技术架构猜想根据其描述,Palimpscape 很可能是一个浏览器扩展(Chrome Extension 或 Firefox Add-on)。其核心组件应包括:
manifest.json:扩展的配置文件,声明权限(如访问页面数据、拦截网络请求)。- Content Script(内容脚本):注入到网页中的 JavaScript 文件,负责在页面上执行 DOM 操作,查找和替换广告元素。
- Background Script(后台脚本):处理更复杂的逻辑,例如管理词汇库、更新拦截规则。
- 词汇数据文件:一个本地的 JSON 文件或通过 API 获取的在线词库,包含单词、音标、释义、例句等。
下面,我们将基于这个技术猜想,开始动手实践。
3. 环境准备与前置条件
由于 Palimpscape 是一个 Show HN 上的开源项目,我们将以开发者视角,从零开始理解并构建一个类似的原型。这将比单纯介绍用法更有价值。
你需要准备:
- 操作系统:Windows 10/11, macOS, 或 Linux 发行版均可。
- 浏览器:Google Chrome 或 Microsoft Edge(基于 Chromium),因为它们对扩展开发的支持最完善。本文以 Chrome 为例。
- 代码编辑器:VS Code, Sublime Text, WebStorm 等任选。
- 基础技能:了解 HTML、CSS 和 JavaScript (ES6+) 的基础知识。
项目结构初始化首先,创建一个名为palimpscape-demo的文件夹,并建立如下基础结构:
palimpscape-demo/ ├── manifest.json # 扩展配置文件 ├── background.js # 后台脚本(可选,初期可能不需要) ├── content.js # 内容脚本,核心逻辑所在 ├── vocab.json # 词汇库数据文件 ├── popup.html # 扩展弹出窗口界面(用于控制开关等) ├── popup.js # 弹出窗口逻辑 └── icons/ # 扩展图标文件夹 ├── icon16.png ├── icon48.png └── icon128.png我们首先从最核心的manifest.json开始。
4. 核心流程拆解:从识别到替换
一个简化版的 Palimpscape 工作流程可以分为以下四步:
步骤 1:声明权限与注入脚本在manifest.json中,我们必须声明需要访问哪些网站以及操作页面的权限。
步骤 2:加载与准备词汇库在内容脚本或后台脚本中加载词汇数据。为了简单起见,我们可以将词汇库直接打包在扩展中。
步骤 3:扫描与识别广告元素这是最具挑战性的部分。我们需要定义一套规则来匹配广告。初期可以采用简单规则,例如匹配常见的广告容器类名或ID。
步骤 4:执行 DOM 替换找到广告元素后,用我们准备好的词汇 HTML 模板替换其内部内容,并应用CSS样式,使其看起来像一个学习卡片。
接下来,我们用代码实现每一个步骤。
5. 完整示例与代码实现
5.1 配置扩展清单 (manifest.json)
{ "manifest_version": 3, "name": "Palimpscape Demo", "version": "1.0", "description": "A demo adblocker that replaces ads with vocabulary words.", "permissions": [ "activeTab", "scripting" ], "host_permissions": [ "<all_urls>" ], "background": { "service_worker": "background.js" }, "content_scripts": [ { "matches": ["<all_urls>"], "js": ["content.js"], "run_at": "document_idle" } ], "action": { "default_popup": "popup.html", "default_icon": { "16": "icons/icon16.png", "48": "icons/icon48.png", "128": "icons/icon128.png" } }, "icons": { "16": "icons/icon16.png", "48": "icons/icon48.png", "128": "icons/icon128.png" } }代码解释:
manifest_version: 3:使用 Manifest V3,这是 Chrome 扩展的最新规范。host_permissions: [“<all_urls>”]:允许扩展在所有网站上运行。在实际产品中,范围可以缩小。content_scripts:定义了注入到所有页面的脚本content.js,并在页面加载完成后 (document_idle) 执行。action:定义了浏览器工具栏图标和点击弹出的页面。
5.2 构建简易词汇库 (vocab.json)
[ { "word": "Ephemeral", "phonetic": "/ɪˈfem.ər.əl/", "definition": "Lasting for a very short time.", "example": "The beauty of the cherry blossoms is ephemeral." }, { "word": "Serendipity", "phonetic": "/ˌser.ənˈdɪp.ə.ti/", "definition": "The occurrence of events by chance in a happy or beneficial way.", "example": "Finding this old book was pure serendipity." }, { "word": "Ubiquitous", "phonetic": "/juːˈbɪk.wɪ.təs/", "definition": "Present, appearing, or found everywhere.", "example": "Mobile phones are now ubiquitous in daily life." }, { "word": "Eloquent", "phonetic": "/ˈel.ə.kwənt/", "definition": "Fluent or persuasive in speaking or writing.", "example": "She delivered an eloquent speech that moved the audience." } ]5.3 实现内容替换脚本 (content.js)
这是整个扩展的核心逻辑。
// content.js (async function() { 'use strict'; // 1. 加载本地词汇库 let vocabulary = []; try { const response = await fetch(chrome.runtime.getURL('vocab.json')); vocabulary = await response.json(); console.log(`[Palimpscape] Loaded ${vocabulary.length} vocabulary words.`); } catch (error) { console.error('[Palimpscape] Failed to load vocabulary:', error); return; } // 辅助函数:从词汇库中随机选取一个单词 function getRandomWord() { if (vocabulary.length === 0) return null; const randomIndex = Math.floor(Math.random() * vocabulary.length); return vocabulary[randomIndex]; } // 辅助函数:创建词汇卡片的HTML function createVocabularyCard(wordObj) { return ` <div class="palimpscape-card" style=" border: 1px solid #e0e0e0; border-radius: 8px; padding: 16px; margin: 10px 0; background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%); font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; box-shadow: 0 2px 4px rgba(0,0,0,0.1); "> <div style="font-size: 1.4em; font-weight: bold; color: #2c3e50; margin-bottom: 8px;"> ${wordObj.word} </div> <div style="font-size: 0.9em; color: #7f8c8d; margin-bottom: 8px; font-style: italic;"> ${wordObj.phonetic} </div> <div style="font-size: 1em; color: #34495e; margin-bottom: 12px;"> <strong>释义:</strong> ${wordObj.definition} </div> <div style="font-size: 0.95em; color: #2c3e50; border-left: 3px solid #3498db; padding-left: 10px;"> <strong>例句:</strong> “${wordObj.example}” </div> <div style="text-align: right; margin-top: 12px; font-size: 0.8em; color: #95a5a6;"> 🎓 由 Palimpscape 提供 </div> </div> `; } // 2. 定义广告元素选择器(这是一个简化示例,实际规则复杂得多) // 这里组合了常见的广告容器类名、ID和数据属性 const adSelectors = [ '[class*="ad-"]', '[id*="ad-"]', '[class*="ads"]', '[id*="ads"]', '.advertisement', '.ad-container', '[data-ad-type]', // 可以添加更多来自 EasyList 等规则的常见模式 ]; // 3. 主函数:扫描并替换广告 function replaceAds() { console.log('[Palimpscape] Starting ad replacement scan...'); let replacedCount = 0; adSelectors.forEach(selector => { const elements = document.querySelectorAll(selector); elements.forEach(element => { // 检查元素是否可见且有一定尺寸,避免替换隐藏或微小的元素 const rect = element.getBoundingClientRect(); if (rect.width > 50 && rect.height > 20 && element.offsetParent !== null) { const word = getRandomWord(); if (word) { element.innerHTML = createVocabularyCard(word); // 移除可能存在的广告相关事件监听器或样式 element.className = ''; // 清空原有类名 element.id = `palimpscape-replaced-${replacedCount}`; replacedCount++; console.log(`[Palimpscape] Replaced an ad with word: "${word.word}"`); } } }); }); // 4. 使用 MutationObserver 监听动态加载的广告 // 许多现代网站的广告是通过 AJAX 动态插入的,需要持续监听 const observer = new MutationObserver((mutations) => { mutations.forEach((mutation) => { if (mutation.addedNodes.length) { // 简单策略:每次 DOM 变化后,重新扫描新增节点中的广告 // 更高效的策略是只检查新增的节点 mutation.addedNodes.forEach(node => { if (node.nodeType === 1) { // ELEMENT_NODE adSelectors.forEach(selector => { if (node.matches && node.matches(selector)) { const word = getRandomWord(); if (word) { node.innerHTML = createVocabularyCard(word); node.className = ''; } } // 同时检查新节点内部可能包含的广告元素 if (node.querySelectorAll) { const childAds = node.querySelectorAll(selector); childAds.forEach(adElement => { const word = getRandomWord(); if (word) { adElement.innerHTML = createVocabularyCard(word); adElement.className = ''; } }); } }); } }); } }); }); // 开始监听整个 document body 的变化 observer.observe(document.body, { childList: true, subtree: true }); console.log(`[Palimpscape] Initial scan replaced ${replacedCount} ad(s). Observer active.`); } // 等待页面基本加载完成后再执行 if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', replaceAds); } else { replaceAds(); } })();5.4 创建简单的弹出窗口 (popup.html & popup.js)
popup.html
<!DOCTYPE html> <html> <head> <style> body { width: 300px; padding: 15px; font-family: Arial, sans-serif; } .status { padding: 10px; border-radius: 5px; margin-bottom: 15px; text-align: center; font-weight: bold; } .active { background-color: #d4edda; color: #155724; } .controls { display: flex; justify-content: space-between; } button { padding: 8px 15px; border: none; border-radius: 4px; cursor: pointer; font-size: 14px; } #toggleBtn { background-color: #007bff; color: white; } #refreshBtn { background-color: #6c757d; color: white; } </style> </head> <body> <h3>Palimpscape Demo</h3> <div id="status" class="status active">✅ 正在运行</div> <p>将网页广告替换为词汇卡片。</p> <div class="controls"> <button id="toggleBtn">暂停</button> <button id="refreshBtn">手动刷新</button> </div> <script src="popup.js"></script> </body> </html>popup.js
// popup.js document.addEventListener('DOMContentLoaded', function() { const toggleBtn = document.getElementById('toggleBtn'); const refreshBtn = document.getElementById('refreshBtn'); const statusDiv = document.getElementById('status'); let isActive = true; toggleBtn.addEventListener('click', () => { isActive = !isActive; chrome.tabs.query({active: true, currentWindow: true}, (tabs) => { chrome.tabs.sendMessage(tabs[0].id, { action: isActive ? 'activate' : 'deactivate' }); }); if (isActive) { statusDiv.textContent = '✅ 正在运行'; statusDiv.className = 'status active'; toggleBtn.textContent = '暂停'; } else { statusDiv.textContent = '⏸️ 已暂停'; statusDiv.className = 'status'; toggleBtn.textContent = '启动'; } }); refreshBtn.addEventListener('click', () => { chrome.tabs.query({active: true, currentWindow: true}, (tabs) => { chrome.tabs.sendMessage(tabs[0].id, {action: 'refresh'}); statusDiv.textContent = '🔄 已发送刷新指令'; setTimeout(() => { statusDiv.textContent = isActive ? '✅ 正在运行' : '⏸️ 已暂停'; }, 1000); }); }); });同时,需要更新content.js以接收来自弹出窗口的消息:
// 在 content.js 末尾添加 // 5. 与弹出窗口通信 chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { if (request.action === 'deactivate') { // 可以停止 MutationObserver 或标记为不活动状态 console.log('[Palimpscape] Deactivated by user.'); // 简单实现:移除所有 Palimpscape 卡片的特定样式?更复杂的实现需要状态管理。 } else if (request.action === 'activate') { console.log('[Palimpscape] Activated by user.'); replaceAds(); // 重新运行替换逻辑 } else if (request.action === 'refresh') { console.log('[Palimpscape] Manual refresh requested.'); // 可以重新扫描页面,即使广告已被替换过 // 注意:这里需要避免重复替换已处理的元素,可以添加一个数据属性标记 replaceAds(); } });6. 运行结果与效果验证
6.1 加载扩展程序
- 打开 Chrome 浏览器,进入
chrome://extensions/。 - 开启右上角的“开发者模式”。
- 点击“加载已解压的扩展程序”。
- 选择你创建的
palimpscape-demo文件夹。 - 扩展程序列表中应该会出现 “Palimpscape Demo”。
6.2 测试效果
访问一个包含广告的测试页面(例如,一些新闻网站或博客)。
如果页面有匹配我们简单规则(如
[class*="ad-"])的广告元素,你会看到它们被替换成类似下图的词汇卡片:Ephemeral /ɪˈfem.ər.əl/ 释义: Lasting for a very short time. 例句: “The beauty of the cherry blossoms is ephemeral.” 🎓 由 Palimpscape 提供(一个带有渐变背景、圆角边框和清晰排版的卡片)
打开浏览器的开发者工具(F12),切换到Console标签页。你应该能看到来自
content.js的日志,例如[Palimpscape] Loaded 4 vocabulary words.和[Palimpscape] Replaced an ad with word: “Ubiquitous”。点击浏览器工具栏中的扩展图标,弹出窗口应显示“正在运行”,并可以通过按钮控制开关和手动刷新。
6.3 验证动态广告拦截
- 访问一个广告是异步加载的页面(如无限滚动的社交媒体或视频网站)。
- 滚动页面,触发新内容加载。
- 观察新加载出来的广告是否也被成功替换。查看 Console 日志,确认
MutationObserver是否正常工作。
7. 常见问题与排查思路
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 扩展加载失败 | manifest.json格式错误或缺少必要字段。 | 在chrome://extensions/页面查看错误信息(通常为红色)。 | 检查 JSON 语法,确保manifest_version,name,version等字段正确。使用 JSON 验证工具。 |
| 广告未被替换 | 1. 广告元素的选择器不匹配。 2. 内容脚本未正确注入。 3. 页面在脚本执行后才加载广告。 | 1. 打开开发者工具(F12),使用Elements面板检查广告容器的类名或ID。2. 在 Console中查看是否有[Palimpscape]开头的日志。3. 检查 Network面板,看广告是否是后续请求加载的。 | 1. 更新adSelectors数组,添加更精准的选择器。2. 确认 manifest.json中content_scripts的matches字段包含当前网站。3. 确保 MutationObserver已正确配置并运行。 |
| 词汇卡片样式错乱 | 1. 原广告元素的CSS样式影响了卡片。 2. 内联样式被覆盖。 | 1. 使用开发者工具的Elements和Styles面板,检查替换后元素的最终计算样式。2. 查看是否有 !important的样式规则冲突。 | 1. 在createVocabularyCard函数中,使用更具体、更高权重的内联样式,或添加!important。2. 在替换后,彻底清除原元素的 class和style属性(代码中已做)。 |
| 页面布局塌陷或抖动 | 替换后的卡片尺寸与原广告差异过大。 | 观察替换前后元素的offsetHeight和offsetWidth。 | 在替换时,尝试固定卡片的宽高,或使用min-height/min-width来稳定布局。可以在卡片样式中添加min-height: ${rect.height}px;。 |
| 扩展图标不显示 | 图标文件路径错误或尺寸不符。 | 检查icons文件夹是否存在,且图片格式为 PNG。 | 确保manifest.json中指定的图标路径正确,并提供了 16x16, 48x48, 128x128 三种尺寸的图标。 |
| 弹出窗口无法通信 | popup.js与content.js的消息传递错误。 | 在popup.js和content.js中添加console.log检查消息是否发送和接收。 | 确保content.js中已正确添加chrome.runtime.onMessage监听器。发送消息时,tabs[0].id必须正确。 |
8. 最佳实践与工程建议
将 Palimpscape 从一个演示原型发展为健壮的工具,需要考虑以下工程化实践:
1. 广告识别策略
- 不要重复造轮子:集成成熟的过滤列表,如EasyList、EasyPrivacy。可以定期从这些项目的仓库下载规则,并将其转换为可执行的 CSS 选择器或网络请求过滤规则。
- 分层策略:结合静态规则(CSS选择器)和动态规则(网络请求拦截)。优先拦截网络请求,从源头阻止广告资源加载,然后再用内容脚本清理残留的占位元素。
- 机器学习辅助(进阶):可以尝试使用轻量级模型(如 TensorFlow.js)对页面元素进行图像或文本特征分析,辅助判断是否为广告,但这会显著增加扩展复杂度。
2. 词汇库管理
- 分级词库:根据用户水平(如四六级、托福、GRE)提供不同难度的词库。
- 上下文相关:尝试分析页面主题(如科技、医学、金融),并展示相关领域的词汇。
- 离线优先,在线更新:将基础词库打包在扩展中保证可用性,同时提供在线更新通道,增加新词或用户自定义词汇。
- 多样化内容:不止于单词,可以替换为名言警句、历史小知识、编程小技巧等。
3. 性能与用户体验
- 节流与防抖:
MutationObserver的回调可能被频繁触发,必须使用防抖(Debounce)或节流(Throttle)技术来避免性能问题。 - 懒加载与虚拟化:如果页面广告极多,一次性全部替换可能卡顿。可以只替换视口(Viewport)内的广告,随着滚动再替换后续的。
- 用户控制:提供丰富的设置选项:开关替换功能、选择词库、设置屏蔽网站白名单/黑名单、自定义卡片样式。
- 无干扰模式:提供一种模式,仅在最显眼或最恼人的广告位置进行替换,而非所有广告位。
4. 代码架构
- 模块化:将广告检测器、词汇管理器、DOM 操作器、样式管理器分离成独立模块。
- 配置化:所有规则、选择器、样式都通过配置文件管理,便于更新和维护。
- 错误边界:使用
try...catch包裹核心操作,避免因某个广告元素异常导致整个脚本崩溃。 - 日志与调试:在生产版本中移除
console.log,但保留通过配置开启的详细日志功能,方便用户反馈问题。
5. 隐私与安全
- 最小权限原则:在
manifest.json中,host_permissions不要滥用<all_urls>,可以考虑改为["*://*/*"]并在安装时说明,或使用activeTab权限动态申请。 - 不收集用户数据:明确声明扩展不会收集用户的浏览历史、页面内容等隐私信息。词汇库更新也应使用公开、匿名的方式。
- 内容安全:确保替换内容(词汇、例句)来源可靠,避免注入恶意代码或不当信息。
9. 总结与后续学习方向
通过构建 Palimpscape 的原型,我们完成了一次从产品创意到技术实现的完整探索。它不仅仅是一个“去广告”工具,更代表了一种建设性干扰的设计思想:在消除负面体验的同时,创造正面价值。
本文的核心收获:
- 理解了广告拦截的基本原理:基于规则的DOM过滤和网络请求拦截。
- 掌握了浏览器扩展的核心开发流程:从
manifest.json配置,到content_script注入与DOM操作,再到前后台通信。 - 实践了动态内容处理的关键技术:使用
MutationObserver监听并处理异步加载的内容。 - 学习了如何将抽象想法落地为可运行的代码,并考虑了性能、用户体验和工程化问题。
你可以继续深入的方向:
- 深入研究广告过滤规则:阅读 EasyList 等项目的文档和规则语法,实现一个本地的规则解析引擎。
- 探索 Service Worker 进行网络拦截:在 Manifest V3 中,使用
declarativeNetRequestAPI 进行更高效、更安全的网络请求拦截,这是现代广告拦截器的核心。 - 设计更精美的UI与交互:让词汇卡片支持点击发音、收藏单词、显示更多例句等功能。
- 构建后端服务:开发一个简单的API,用于管理词库、接收用户反馈、进行A/B测试等。
- 发布与分发:学习如何将扩展打包并提交到 Chrome Web Store 或 Firefox Add-ons 平台。
技术工具的价值,最终体现在它如何塑造我们的行为与环境。Palimpscape 提供了一个有趣的范式:我们的工具不仅可以帮我们拒绝什么,更可以引导我们关注什么。下次当你再看到网页上的广告时,或许可以想一想,那片空间,是否能有更好的用途?
建议将本项目的代码收藏或 Fork 到你的 GitHub,作为学习浏览器扩展开发和前端工程化的一个绝佳起点。在实际修改和扩展它的过程中,你会遇到更多真实的问题,而解决这些问题的过程,正是成长的阶梯。