智能助手网
标签聚合 入门

/tag/入门

linux.do · 2026-04-18 22:39:15+08:00 · tech

小白开发插件,在期间看文件的时候学到的一些东西,特地分享给大家: 博文链接: https://418122.xyz/posts/project/chrome-extension-basic-structure-beginners-guide 插件仓库链接: https://github.com/V-IOLE-T/tab-harbor GitHub 正在尝试上架 Chrome 插件商店 很多人第一次打开一个 Chrome 插件项目时,会看到一堆文件名,然后立刻陷入混乱。 index.html 、 style.css 、 manifest.json 、 background.js 、 content.js 、 app.js 看起来都像"代码文件",可它们根本不在同一个层面上工作。想真正看懂插件,关键不是背文件名,而是理解这些文件分别处在什么位置,承担什么职责,以及它们如何配合浏览器、网页和用户界面一起运转。 这篇文章想做的事很直接,就是把这些文件背后的逻辑彻底串起来,让你在看到一个插件目录时,能迅速判断每个文件到底在做什么。 index.html 是插件界面的骨架 在 Chrome 插件里, index.html 通常用来描述某个页面的内容和结构。它决定页面上会出现什么元素,比如标题、按钮、输入框、文本区域,也负责把样式文件和脚本文件接进来。你可以把它理解为一个界面的骨架,因为页面里"有什么"这件事,主要就是由 HTML 决定的。 如果一个插件有弹窗界面,那么点击插件图标后看到的内容,通常来自某个 HTML 文件。有些项目把它命名为 popup.html ,有些项目也会叫 index.html 。名字并不重要,重要的是它是否被浏览器当成插件页面真正加载了。 下面这个例子足够说明它的角色: <!DOCTYPE html> <html> <head> <link rel="stylesheet" href="style.css"> </head> <body> <h1>Hello Chrome Extension</h1> <button>点击我</button> </body> </html> 这段代码里,页面里有哪些内容,完全由 HTML 决定。它告诉浏览器,这个页面有一个标题,有一个按钮,同时还引入了样式文件。你在页面里看到的一切界面元素,本质上都从这里开始。 style.css 决定界面看起来是什么样 如果说 HTML 负责把页面元素摆出来,那么 style.css 负责让这些元素有可读性、有层次感,也更像一个真正能用的界面。字体大小、颜色、背景、边距、按钮的外观、元素之间的排列方式,这些都属于 CSS 的领域。 比如下面这段代码: body { width: 250px; font-family: Arial, sans-serif; padding: 10px; } h1 { color: blue; font-size: 18px; } button { background-color: green; color: white; } 这段样式并没有改变页面里"有什么",但它显著改变了页面"长什么样"。这正是 CSS 的作用。很多初学者一开始会把 HTML 和 CSS 混着理解,实际上它们解决的是两个完全不同的问题。HTML 决定内容和结构,CSS 决定视觉和排版。 放到插件环境里也是一样。无论这个页面是弹窗页、选项页,还是插件扩展出来的其他界面,CSS 的职责都很稳定,就是把原本生硬的结构变成可以阅读、可以操作、也更符合界面习惯的样子。 在插件里,HTML、CSS、JavaScript 分别站在不同位置上 只看 HTML 和 CSS,还只是理解了插件界面的静态部分。一个真正可用的插件,还必须让界面"动起来",这时候 JavaScript 才会加入进来。 HTML 负责搭出页面结构,CSS 负责赋予它视觉效果,JavaScript 负责让页面对用户的操作做出反应。比如用户点击按钮后获取信息,或者把某个结果显示到页面上,这些都属于 JavaScript 的工作。 下面这段代码很简单,但它能准确展示 JavaScript 的作用: document.getElementById("btn").addEventListener("click", () => { console.log("按钮被点击"); }); 这时候你就能看到三者之间的协作关系。HTML 放了一个按钮,CSS 把这个按钮变得更清晰、易用,JavaScript 让这个按钮具备"点击以后发生事情"的能力。它们共同组成了插件界面这一层的完整逻辑。 manifest.json 是插件的入口和规则中心 当你把眼光从界面移开,就会看到插件最核心的配置文件,也就是 manifest.json 。这个文件的重要性非常高,因为 Chrome 浏览器安装和加载插件时,最先读取的就是它。没有它,插件无法被识别。写错了它,插件也可能根本无法运行。 它承担的职责可以概括为一件事,就是告诉浏览器:这个插件是谁,它有哪些页面,有哪些脚本,想申请哪些权限,以及这些能力应该如何被组织起来。 最简单的内容通常长这样: { "name": "My Extension", "version": "1.0", "manifest_version": 3 } 这里记录了插件的基本身份信息。接着你还会看到它声明插件弹窗页面: { "action": { "default_popup": "popup.html" } } 浏览器读到这里,就知道用户点击插件图标时,应该打开 popup.html 。如果插件带后台脚本,还会出现类似配置: { "background": { "service_worker": "background.js" } } 如果插件需要把脚本注入网页,则可能是这样: { "content_scripts": [ { "matches": ["https://*/*"], "js": ["content.js"], "css": ["content.css"] } ] } 如果插件要访问某些浏览器能力,还得显式申请权限: { "permissions": ["storage", "tabs", "activeTab"] } 所以,从更本质的角度看, manifest.json 的作用,就是定义插件"能做什么、在哪里做、通过谁去做"。这也是为什么它像一个总开关。你后面看到的页面、脚本、权限,最终都要回到这里去确认。 background.js 像插件的后台调度中心 理解了 manifest.json 后,再去看 background.js 就容易多了。这个文件通常不负责展示界面,也不会直接嵌在某个网页里。它更像插件的后台控制层,负责监听浏览器事件、处理全局逻辑、协调不同模块之间的通信。 比如插件刚被安装时,它可以执行初始化逻辑: chrome.runtime.onInstalled.addListener(() => { console.log("插件已安装"); }); 它也可以监听某些全局事件,或者接收来自界面页和内容脚本的消息: chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { if (message.type === "getData") { sendResponse({ data: "这是后台返回的数据" }); } }); 为什么插件需要这样的文件?因为有些事情不适合写在界面脚本里,也不适合写在网页注入脚本里。比如统一管理状态、协调多个标签页、处理浏览器级别的事件、访问某些只允许后台使用的 API,这些任务都更适合放在后台脚本中。 如果你使用的是 Manifest V3,那么这里还有一个重要变化。 background.js 在很多情况下是以 service_worker 的方式运行的。它不会一直常驻,而是事件来了就唤醒,执行完任务后可能进入休眠。这背后体现的是 Chrome 的设计取向,它希望插件更节省资源,也更容易控制风险。 content.js 是插件进入网页内部后的执行者 如果说后台脚本站在浏览器层面处理逻辑,那么 content.js 站在网页现场工作。它会被注入某个网页中,因此可以直接访问这个网页的 DOM,也就是页面上的标题、按钮、正文、输入框这些真实存在的元素结构。 举个最简单的例子: const title = document.querySelector("h1"); console.log(title.innerText); 这段代码能直接读取网页上的标题内容。它也可以修改页面: document.body.style.backgroundColor = "lightyellow"; 甚至还能监听页面中的某些操作。也就是说, content.js 的核心价值,在于让插件真正进入网页环境,看到页面内容,并对页面进行读取或修改。 不过这里有一个非常容易被忽略的边界。 content.js 虽然运行在网页里,但它依然属于插件。它拥有插件赋予的能力,也受到插件环境的限制。它和页面原生脚本之间并不是完全共享一切的关系,因为浏览器会通过隔离机制防止它们互相污染。这个细节很关键,因为很多初学者会误以为内容脚本和网页自身的 JavaScript 完全是一回事,实际情况并没有这么简单。 插件的核心难点,在于多个运行环境同时存在 当你把 popup.js 、 background.js 、 content.js 放在一起看时,很容易觉得它们全都是 JavaScript,所以好像只是写法不同。真正的区别并不在语法,而在运行环境。 界面脚本运行在插件自己的页面里,只有当这个页面被打开时它才活跃。后台脚本运行在插件后台,专门处理全局事件和中转逻辑。内容脚本运行在目标网页中,负责接触页面本身。这三种脚本虽然都写成 .js 文件,但它们能访问的对象、拥有的权限、存在的生命周期都不一样。 这正是 Chrome 插件学习曲线真正陡峭的地方。你卡住的往往不是 API,而是脑中没有建立"多环境协作"的图景。一旦建立起这个图景,再看文件结构就会清晰很多。 一个最常见的协作流程,到底是怎样跑起来的 假设我们做一个非常简单的插件。用户点击插件图标,弹出一个小窗口,窗口里有一个按钮,点击按钮后读取当前网页标题并显示出来。这个功能很小,但它足以把前面提到的所有角色串到一起。 首先浏览器会读取 manifest.json : { "manifest_version": 3, "name": "Title Reader", "version": "1.0", "action": { "default_popup": "popup.html" }, "permissions": ["activeTab"], "background": { "service_worker": "background.js" } } 这一步完成后,浏览器已经知道这个插件长什么样,有什么弹窗页面,有什么后台脚本,以及它申请了当前标签页权限。 当用户点击插件图标时,浏览器会根据 default_popup 打开 popup.html 。页面一打开,HTML 会把结构渲染出来,CSS 负责样式,页面脚本负责交互逻辑。如果 popup.html 里有一个按钮和一个显示结果的区域,那么脚本就可以写成这样: document.getElementById("btn").addEventListener("click", async () => { const [tab] = await chrome.tabs.query({ active: true, currentWindow: true }); document.getElementById("result").textContent = tab.title; }); 如果需求只是读取当前标签页的标题,这样就够了。但如果你想读取网页内部更细的内容,比如某段正文、某个按钮文本、某个元素属性,那仅靠弹窗脚本通常不够,你就得让 content.js 进入网页现场去执行。 它可以先读取网页内容,然后把结果通过消息机制发回插件系统: const pageTitle = document.title; chrome.runtime.sendMessage({ type: "pageTitle", data: pageTitle }); 这时候如果流程稍微复杂一些,后台脚本就会出场,承担协调者角色。比如弹窗先给后台发消息,后台再联系当前标签页里的内容脚本,内容脚本拿到网页数据后返回给后台,后台再把结果转给弹窗。这个链路看起来绕了一层,但你会发现它的分工很清晰。界面只处理用户交互,后台处理协调和调度,内容脚本只关注网页现场。 示例代码大致可以写成这样。 popup.js : chrome.runtime.sendMessage({ type: "getPageInfo" }, (response) => { document.getElementById("result").textContent = response.data; }); background.js : chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { if (message.type === "getPageInfo") { chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => { chrome.tabs.sendMessage(tabs[0].id, { type: "readTitle" }, (response) => { sendResponse({ data: response.title }); }); }); return true; } }); content.js : chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { if (message.type === "readTitle") { sendResponse({ title: document.title }); } }); 把这段流程真正看懂后,你对插件的整体架构就已经入门了。因为你会意识到,插件开发的本质不是单纯地写一个页面,而是把浏览器环境、插件界面和网页环境连接成一个系统。 app.js 到底是什么,它为什么总让人困惑 很多人学到这里,又会看到一个新的文件名,叫 app.js ,然后开始怀疑自己是不是漏学了某种"官方角色"。其实这里最需要澄清的一点是, app.js 并不是 Chrome 插件规范里规定必须存在的文件。它通常只是开发者自己起的名字。 这意味着,当你在一个插件项目里看到 app.js 时,不能直接根据名字判断它一定负责什么。判断它职责的关键,始终只有两件事,就是它在哪里被加载,以及它运行在什么环境中。 如果 popup.html 里有这样的代码: <script src="app.js"></script> 那这个 app.js 很可能就是弹窗页面的主逻辑脚本。它可能负责监听按钮点击、获取输入框内容、调用浏览器 API、更新页面文本等交互行为。比如: document.getElementById("btn").addEventListener("click", async () => { const [tab] = await chrome.tabs.query({ active: true, currentWindow: true }); document.getElementById("result").textContent = tab.title; }); 如果它被 options.html 引入,它就可能是设置页脚本。如果它出现在 manifest.json 的后台配置中: { "background": { "service_worker": "app.js" } } 那它虽然名字叫 app.js ,实际承担的就是后台脚本的职责。如果它被声明在 content_scripts 中: { "content_scripts": [ { "matches": ["<all_urls>"], "js": ["app.js"] } ] } 那它本质上就是内容脚本。 所以,理解 app.js 最重要的一句话是, 文件名本身不决定身份,加载位置和运行环境才决定身份 。很多初学者容易被文件名带偏,以为名字像什么就是什么。实际开发里, main.js 、 index.js 、 app.js 这类名字都很常见,它们更多反映的是工程命名习惯,而不是浏览器规范。 判断一个脚本作用时,最可靠的方法是什么 如果你以后再打开一个陌生的插件项目,最稳妥的阅读方式就是先看 manifest.json ,再看 HTML 文件,最后才去看具体脚本内容。 manifest.json 会告诉你有哪些页面、有哪些后台脚本、有哪些内容脚本、申请了哪些权限。HTML 文件会告诉你哪些 JavaScript 是页面脚本,因为它们会被 <script src="..."> 直接引入。脚本内容本身才会进一步告诉你,这个文件内部具体写了什么业务逻辑。 这个阅读顺序很重要,因为它会强迫你从"运行上下文"去理解代码,而不是从"文件名猜测"去理解代码。你一旦形成这种习惯,不光看 Chrome 插件会快很多,将来学 Vue、React,甚至看更复杂的前端工程,也会有更强的拆解能力。 学插件时,最推荐的理解路径 对于初学者来说,最容易迷失的地方,是一上来就想把所有文件和 API 全部记住。这样做通常效果很差,因为你的脑海里没有一张系统图,记忆就没有挂钩点。 更好的路径,是先看清谁负责界面,也就是 HTML、CSS 和页面脚本。然后理解谁负责进入网页读取和修改内容,也就是 content.js 。最后再理解谁负责监听全局事件、连接各个模块、处理中间调度,也就是 background.js 。当这条主线建立起来以后,消息传递、权限管理、脚本注入这些内容都会顺势变得可理解。 如果你一定要用一句话概括整个插件结构,那么这句话可以写成这样: manifest.json 先注册角色并声明权限,HTML 和 CSS 负责界面呈现,JavaScript 负责交互逻辑, background.js 负责后台调度, content.js 负责进入网页执行具体动作,而 app.js 是否重要,取决于它究竟被谁加载、运行在哪个环境里。 结尾:真正该建立的,是"环境意识" 学 Chrome 插件,表面上像是在学很多零散文件。更深一层看,你其实是在学习多个受控运行环境如何协作。浏览器层、插件界面层、网页内容层,这三层有各自的边界,也通过消息和配置互相连接。你一旦抓住这个系统视角,就不会再被文件名和目录结构轻易迷惑。 所以当你下次看到一个插件项目时,先别急着问"这个文件名是什么意思"。更值得问的是,这个文件由谁加载,它运行在哪里,它能访问什么,它和谁通信。只要这四个问题想清楚,整个项目的骨架就会慢慢显形。 【拓展思考】 Chrome 插件很像一个小型多进程系统。弹窗脚本、后台脚本、内容脚本分别处在不同上下文中,消息传递像协议,权限声明像访问控制,页面注入像受限部署。你如果从系统设计的视角理解插件,后面学浏览器扩展、Electron、前端工程化,很多概念都会互相打通。 2 个帖子 - 2 位参与者 阅读完整话题

www.ithome.com · 2026-04-18 13:39:52+08:00 · tech

IT之家 4 月 18 日消息,韩媒 The Elec 昨日(4 月 17 日)发布博文,报道称三星停止生产 LPDDR4 和 LPDDR4X 内存芯片, 将资源全面转向利润更高的 LPDDR5 和 LPDDR5X。 IT之家援引博文介绍,对于目前已确定的旧订单,三星将会继续履行生产 ,但后续不再接收新的 LPDDR4 出货请求,要求客户转向新标准。 该媒体指出三星此举对供应链产生连锁反应,让高通、联发科等芯片制造商必须调整产品规划。而对于普通消费者来说,在内存超级周期的大背景下,入门手机本来就首当其冲, 而三星升级 LPDDR5 系列,让入门手机市场“雪上加霜”,进一步推高消费者选购价格。 三星自家移动部门也被迫采购 LPDDR5,计划升级低端 Exynos 芯片组,意味着 Galaxy A17 等入门手机也将配备 LPDDR5 内存, 虽然获得了更快的带宽速度,但硬件成本的上升极有可能转嫁给消费者。

linux.do · 2026-04-16 12:47:15+08:00 · tech

本人低于小白水品,野生入门水平,大佬们随便看看。 之前站里有claude 免费api用起来感觉真不错,后来公益不行了,好久没用上了,最近使用gptplus感觉差距不少,偶尔会看到佬们通过claude code调用其他的模型,但不是得改配置文件,就是我看不懂@@!没有基础看得和天书似的。 今天突然又想试试claude调用其他模型,看看能不能强一点?没有依据瞎想。 为了省事,主要还是我菜不懂,用ccswitch能让我跑起来这就行了,下面直接截图分享如何使用ccswitch接入gptplus,然后通过claude code来使用,plus这个现在量大管饱。 方案一:使用官网gpt账户 再说一句我得理解可能有问题:claude调用其他模型过程 错了勿喷,野生白鼠理解 claude code → ccswitch → ccswitch 转换器 → gptplus 开启cc得转换器功能,将claude模型请求变成gpt格式,ccswitch它叫代理,我换了个称呼 登录网页授权 方案二:使用第三方api 再说一句我得理解可能有问题:claude调用其他模型过程 错了勿喷,野生白鼠理解 claude code → ccswitch → ccswitch 转换器 → sub2api → chatgpt 还是开启代理转换器功能,非上网哪个代理!!! 添加第三方api 启用代理转换器 测试 方案三:使用第三方api 这个是自建sub2api,使用sub2api中得模型映射功能,这个似乎思考只能达到high不能xhigh,佬们自行研究吧 再说一句我得理解可能有问题:claude调用其他模型过程 错了勿喷,野生白鼠理解,帮助想使用claude code 配合GPT得佬快速使用,主要还是我用不起A/啊。 claude code → ccswitch → sub2api → sub2api 转换器 → chatgpt 模型似乎会自动映射,当然你可以自行修改你需要得映射关系 我理解是对与claude code来说它调用得就是A/得模型,它本身不知道是GPT,所以这里得链接和api是你自行配置,其他都是按照官方格式默认就行。 这里关闭ccswitch得转换,因为我们使用的是sub2api得转换器 最后补一句吧,方案一调用得是官方走官方通道,记得将你们得 开起来!!! 1 个帖子 - 1 位参与者 阅读完整话题

linux.do · 2026-04-16 12:41:27+08:00 · tech

飞升三级了!非常高兴! 最近对认知神经科学、神经精神分析还有神经生物学基质这方面的内容特别感兴趣,到b站上搜了相关的课程,现在再看焦傅金教授的《当代认知神经科学:脑与心智2025》这门课,感觉还是非常适合新手的,但听到现在感觉好像也有点点浅,更像是通识教育课程。 想了解一下站内有没有佬友是专业研究这一方面的,有什么比较推荐的经典著作吗?焦老师的这门课适合新手入门吗? 我可能会比较感兴趣神经生物学基质、脑科学这方面的内容,现在我认为有这样一个路径:“ 1神经生物学基质 → 2认知/情感功能系统 → 3主体经验与人格/症状 ”,以前我看过一点点弗洛伊德、荣格还有阿德勒的内容,基本上都是在第2、3层做讨论,所以我现在可能更希望能了解更基础的神经生物学、脑科学的知识,希望能得到专业佬友的指点 题外话: 之前发过帖子说浏览数据一直不增加,后来发现了原来是我平时读贴的时候喜欢先中键选出一些帖子当做备选阅读,然后再切换到对应标签页去读,但是这样是没法增加阅读数的! 要增加阅读数得每一个帖子都单独点进去阅读。 1 个帖子 - 1 位参与者 阅读完整话题

linux.do · 2026-04-15 09:49:49+08:00 · tech

Apple将推入门级主机Mac Neo!A19Pro+12GBRAM,299美元起 为缓解Mac mini供货偏紧的局面,Apple正筹备推出一款全新入门级主机Mac Neo,意在覆盖普通用户的日常使用需求,进一步完善主机产品布局。 从产品定位来看,Mac Neo将放在Mac mini之下,适配轻量化的日常应用场景。该装置计划搭载与iPhone 17 Pro系列同源的A19 Pro处理器,标配12GB RAM。依托Apple成熟的A系列处理器量产体系,Mac Neo不仅能维持稳定的供货节奏,还可以避开高阶算力与RAM资源的争夺,为压低售价留出空间。 在性能层面,A系列处理器凭借优秀的单核表现,可流畅应对网页浏览、文档处理等高频基础操作,足够胜任家庭与办公环境中的轻度工作。当然,受限于A系列处理器在持续高负载下的功耗与带宽设计,它在专业视频渲染或大规模代码编译场景下的表现无法与配备M系列处理器的Mac mini相提并论。但Mac Neo精准服务于非专业用户,在性能与日常实用性之间实现了较好平衡。 设计方面,Mac Neo延续紧凑机身理念,体积与Apple TV 4K 相近,并可能采用彩色塑料外壳,兼顾简约外观与便携特性。两个传输速率达10Gb的USB-C接口将承担起外接显示与数据传输需求,整机功耗控制在35W,兼具实用性与低能耗优势。 售价上,Mac Neo预计以299美元起步,相比当前599美元起售的Mac mini显得十分优惠。这一策略不仅完善了苹果在入门级桌面市场的产品线,更可能凭借完整的iCloud与连续互通生态,从占据绝对份额的Windows廉价迷你主机阵营中抢夺用户。 文章來源: Qooah.com 2 个帖子 - 2 位参与者 阅读完整话题

linux.do · 2026-04-14 23:08:54+08:00 · tech

锻炼对自己产品的理解 初学者刚入门时,很容易被琳琅满目的原型工具和一堆案例牵着走,脑子里装满了别人花哨的功能、亮眼的方案,却唯独缺少对自己产品的理解,想来想去都是别人的东西。 这样的 PM,平时和别人对话时,往往会是这种状态: A(老板):你觉得这里放四个标签页合理不合理? P(PM):当然合理,美团、点评、微信都这么做。这是大趋势。 A:那我们页面上功能排版这样靠谱不? P:没问题。现在这样的排版方式最常见,交互方式也很流行,色调设计还是国际上流行的 Minimal。 A:额,那我们这几个功能是必需的吗? P:这几个功能竞品都有,我们没有的话不太合适吧。 A:…… 对自己产品的理解,其实可以分成很多层面: 对公司定位的理解,和老板、投资方有关 对用户定位的理解,和公司定位、市场状况有关 对产品定位的理解,和用户定位、产品推出的初衷有关 对公司研发能力的理解,包括设计能力、开发能力和运营能力 对其他部门状况的理解,包括各部门在做什么、当前推进到什么状态 …… 总之,你做出的每一个判断,都应该建立在对产品多方面的理解之上,不能只抓住竞品、市场这些零散因素。 只有这样,当设计师交出一版你不满意的稿子时,你才能说: 这个风格可能更适合年轻人,但我们的目标用户是商务人士。 当你要求工程师去处理一个看起来不重要的 Bug 时,你也可以说: 后续运营部门计划有几次大型活动,到时流量会瞬间暴涨,这个 Bug 会被放大很多倍,所以风险很高。 当你和老板讨论要不要加一个功能时,你能够说: 我觉得这个功能是一个重要补充,和下个版本要做的事情是连得上的。 具体怎么去锻炼,其实很费力,也很费脑。你要不断和老板沟通、和同事沟通,去和用户聊,去做调研,同时做大量思考和总结。你最好有一本属于自己的“词典”,记录所有和你负责产品相关的解释与判断,随时可以翻阅。 这本词典可以记在心里,也可以整理成笔记。别人看来,它可能不起眼,也没有那些“人机交互趋势”“立体化设计语言”之类的术语那么唬人。但如果没有这本词典,你往前走的每一步都可能踩坑,等走远了再回头补,往往已经来不及了。 提高分析和判断能力 如果说对产品的理解是第一步,那么第二步,就是基于这些理解做出靠谱的判断。 很多 PM 遇到问题时,习惯参考别人、做用户调研,或者直接听老板的。但实际上,很多事情并没有那么复杂,只要有生活常识,有基础的推理能力,完全可以先做出更靠谱的初步判断。先有一个初步判断,再去做用户调研,这时候调研才真正有意义:它是验证手段,不是需求来源。 总结起来,就是三个字:讲道理 产品经理在做任何决定时,都应该尽量做到这一点。 那怎样培养这种能力?方法其实也不复杂,就是在日常生活里多问几个“为什么”。 比如: 这个小区的停车场入口为什么要绕三个圈? 优衣库一进门最先看到的为什么常常是内衣裤? 楼下咖啡店平时看着顾客不多,为什么能撑五年还没关门? 很多道理是相通的。你每天都在想明白一些事情,真遇到棘手的产品问题时,思考自然会比别人更远,也更稳。 看别人的产品也一样,别只学表面。多去想它背后的原因:从商业模式到交互细节,再到视觉风格,它为什么这么做,解决的是什么问题,这样的代价又是什么。 3. 确保良好的沟通和表达能力 当你已经对产品有足够理解,也能对问题做出清晰、有依据的判断,下一步就是把这些判断和理解有效地传达给同事。 这件事说难也难,说简单也简单。核心还是两点:一是沟通时要让人愿意配合,二是每次沟通都尽量有效率。 在协作中,尽量做到这些 始终清楚自己是产品经理,但不是“经理”,不要盛气凌人 用事实和理由说服别人,不要动不动搬出“老板决定了” 别人遇到困难时,先理解问题,再一起找备选方案 项目组里谁和谁有了矛盾,要主动协调,不要围观 让产品线上的同事随时知道整体进展,帮助大家建立全局意识 用户反馈整理后及时同步,让团队感受到自己的工作是有价值的 在讨论问题时,也尽量做到这些 每次沟通前先想清楚目的,心里有基本框架 描述问题尽量清晰,不要一边说一边散 每个问题都尽量形成结论,避免讨论很多却什么都没定下来 不要把所有决定都推给别人,自己要准备方案和建议 过程和结论都尽量留下记录,方便后续查阅 交际能力提升最快的办法,往往也不是背书或者记技巧,而是观察一个你觉得相处起来很舒服、同时又很优秀的人,看看他是怎么处理关系、怎么表达观点、怎么推进事情的,然后一点点学过来。 4. 对产品的责任心 责任心看起来像是最容易具备的东西,但真正做到,其实很难。 产品经理最重要的能力,就是让正确的事情相继发生。 你把前面说的那些都做好了,其实只是完成了“做正确的事”这一部分。至于这些事能不能真的推进下去、一个接一个地发生,就要看责任心了。 比如: 午夜工程师为项目上线加班时,你是在楼下看电影,还是愿意留在旁边一起盯着? 设计师和老板因为方案僵住时,你是在等他们自己缓和,还是主动去沟通、推动事情往前走? 运营同事提了一个很赶、又不太现实的需求时,你是直接一句“做不完”,还是愿意坐下来一起找折中的办法? 从劳动契约上说,你当然没有义务包办一切;从岗位职责上说,也不是所有问题都归你解决;公司有坑,也不必全由你来填。 但一个真正优秀的产品经理,会对所有和产品相关的事务保持敏感,会把那些妨碍产品正常推进的事情看成自己需要介入的问题,也会去关注那些可能真正帮助产品变好的机会。 在这些具体问题上的取舍,决定了你最终只是一个普通 PM,还是一个更强的 PM。 5. 关于工具的使用 把工具放在后面说,是因为真正意义上的产品经理,其实不一定要掌握很多原型工具、交互工具,甚至绘图工具。 任何工具都只是服务于人、服务于当下的事情。关键问题始终只有一个: 它到底有没有帮你提高效率。 很多初创团队刚起步,就急着上各种复杂的 Feature/Bug 管理平台,制定很重的需求文档规范,把脑图、Axure 之类的东西当成标配。最后流程越来越重,效率却没有提高,反而拖慢了很多事。 就像 MBA 不太适合创业者一样,过于复杂的流程,往往会严重拖累中小公司的运转。 只要你手头的工具好用、顺手、高效,就没有必要为了显得专业去换成更复杂、更“高级”的东西。 6. 尽力涉猎广泛 作为产品经理,懂一些其他岗位的基础知识其实很有帮助。这样一来,沟通会更顺畅;二来,在考虑需求和方案时,你也更容易有数。 这是一种很好的补充,当然,不是说你非得样样精通。但你至少可以去看看他们平时在看什么书,关注什么内容,学一点基础概念。哪怕只是看看一些入门文章,也会有帮助。 这样至少别人不会觉得,你每天只会空想点子、画点原型、做一些飘在空中的方案。 4 个帖子 - 4 位参与者 阅读完整话题

linux.do · 2026-04-14 21:44:46+08:00 · tech

[TOC] 分块 维护常见的区间操作与查询操作。 将数列分成$\sqrt n$块,每块有$\sqrt n$ 个数字。 核心为:快速维护整块的操作,暴力维护非整块的操作。 要考虑的有三个: 1、不完整的块如何处理 2、完整的块如何处理 3、需要预处理什么信息 数列分块入门1 区间加、单点查询 区间修改:区间内每个数字都加v 单点查询: 整块的:直接累计lazy[i] 非整块的,最多有两个非整块的,直接暴力即可。 int n, a[N]; int len, tot, id[N], lazy[N]; void build() { len = sqrt(n); //块的长度 tot = n / len; //一共有多少块 if (n % len) tot++; for (int i = 1; i <= n; i++) id[i] = (i - 1) / len + 1; } void add(int l, int r, ll x) { // 区间加法 int sid = id[l], eid = id[r]; if (sid == eid) { // 在一个块中 for (int i = l; i <= r; i++) a[i] += x; return; } for (int i = l; id[i] == sid; i++) a[i] += x; for (int i = sid + 1; i < eid; i++) lazy[i] += x; // 更新区间和数组(完整的块) for (int i = r; id[i] == eid; i--) a[i] += x; } int main() { CLOSE cin >> n; for (int i = 1; i <= n; i++) cin >> a[i]; build(); for (int i = 1; i <= n; i++) { int op, l, r, x; cin >> op >> l >> r >> x; if (op == 0) add(l, r, x); else cout << a[r] + lazy[id[r]] << endl; } return 0; } 数列分块入门4 区间加、区间和 区间修改:区间每个数字都加v 区间查询:区间和 修改和之前一样 查询类似于修改,整块直接求和,非整块暴力求和。 int id[N] , n , len; // id 表示块的编号, len=sqrt(n) , 即上述题解中的s, sqrt的时候时间复杂度最优 ll a[N], lazy[N], s[N]; // a 数组表示数据数组, lazy 数组记录每个块的整体赋值情况, 类似于 lazy_tag, s表示块内元素总和 void build() { len = sqrt(n); for (int i = 1; i <= n; i++) { id[i] = (i - 1) / len + 1; s[id[i]] += a[i]; } } void add(int l, int r, ll x) { // 区间加法 int sid = id[l], eid = id[r]; if (sid == eid) { // 在一个块中 for (int i = l; i <= r; i++) a[i] += x, s[sid] += x; return; } for (int i = l; id[i] == sid; i++) a[i] += x, s[sid] += x; for (int i = sid + 1; i < eid; i++) lazy[i] += x, s[i] += len * x; // 更新区间和数组(完整的块) for (int i = r; id[i] == eid; i--) a[i] += x, s[eid] += x; // 以上两行不完整的块直接简单求和,就OK } ll query(int l, int r, ll p) { // 区间查询 int sid = id[l], eid = id[r]; ll ans = 0; if (sid == eid) { // 在一个块里直接暴力求和 for (int i = l; i <= r; i++) ans = (ans + a[i] + lazy[sid]) % p; return ans; } for (int i = l; id[i] == sid; i++) ans = (ans + a[i] + lazy[sid]) % p; for (int i = sid + 1; i < eid; i++) ans = (ans + s[i]) % p; for (int i = r; id[i] == eid; i--) ans = (ans + a[i] + lazy[eid]) % p; // 和上面的区间修改是一个道理 return ans; } int main() { CLOSE cin >> n; for (int i = 1; i <= n; i++) cin >> a[i]; build(); for (int i = 1; i <= n; i++) { int op, l, r, c; cin >> op >> l >> r >> c; if (op == 0) add(l, r, c); else cout << query(l, r, c + 1) << endl; } return 0; } 数列分块入门7 区间加、区间乘、单点查询 区间修改:区间乘法 / 区间加法 单点查询 我们考虑用两个标记,乘法标记 加法标记 令 乘法标记 的优先级高于 加法标记 (如果反过来的话,新的加法标记无法处理) 若当前的一个块乘以 m_1 后加上 a_1 ,这时进行一个乘 m_2 的操作,则原来的标记变成 (m_1 m_2, a_1 m_2) 若当前的一个块乘以 m_1 后加上 a_1 ,这时进行一个加 a_2 的操作,则原来的标记变成 (m_1, a_1 + a_2) int n, q, tot; int id[M], len, L[M], R[M]; ll a[M], mul[M], add[M]; void build() { len = sqrt(n); //块的长度 tot = n / len; //一共有多少块 if (n % len) tot++; for (int i = 1; i <= n; i++) id[i] = (i - 1) / len + 1; for (int i = 1; i <= tot; i++) { //记录每一块的起点和终点 L[i] = (i - 1) * len + 1; R[i] = i * len; mul[i] = 1; } R[tot] = n; //最后一块不一定有len个,末尾是n } void pushdown(int ID) { //乘法标记的优先级高于加法(如果反过来的话,新的加法标记无法处理) for (int i = L[ID]; i <= R[ID]; i++) a[i] = (a[i] * mul[ID] % mod + add[ID]) % mod; mul[ID] = 1; add[ID] = 0; } void update_add(int l, int r, ll x) { // 区间加法 (mul,add+x) int sid = id[l], eid = id[r]; if (sid == eid) { // 在一个块中 pushdown(sid); for (int i = l; i <= r; i++) a[i] = (a[i] + x) % mod; return; } pushdown(sid); pushdown(eid); for (int i = l; i <= R[sid]; i++) a[i] = (a[i] + x) % mod; for (int i = sid + 1; i < eid; i++) add[i] = (add[i] + x) % mod; for (int i = L[eid]; i <= r; i++) a[i] = (a[i] + x) % mod; } void update_mul(int l, int r, ll x) { // 区间乘法 (mul*x,add+x) int sid = id[l], eid = id[r]; if (sid == eid) { // 在一个块中 pushdown(sid); for (int i = l; i <= r; i++) a[i] = (a[i] * x) % mod; return; } pushdown(sid); pushdown(eid); for (int i = l; i <= R[sid]; i++) a[i] = a[i] * x % mod; for (int i = sid + 1; i < eid; i++) add[i] = add[i] * x % mod, mul[i] = mul[i] * x % mod; for (int i = L[eid]; i <= r; i++) a[i] = a[i] * x % mod; } int main() { CLOSE cin >> n; for (int i = 1; i <= n; i++) cin >> a[i]; build(); for (int i = 1; i <= n; i++) { int op, l, r, c; cin >> op >> l >> r >> c; if (op == 0) update_add(l, r, c); else if (op == 1) update_mul(l, r, c); else cout << (a[r] * mul[id[r]] % mod + add[id[r]]) % mod << endl; } return 0; } 数列分块入门2 区间加、区间查询小于x的数字个数 区间修改:区间每个数字都加v 区间查询:区间内小于x的数字个数 考虑每一块内都是有序的。 整块修改:直接加就行,不会影响有序性 非整块修改:暴力加,然后重新排序。 整块查询:二分 非整块查询:暴力即可 int n, q, tot; int id[M], len, L[M], R[M]; ll a[M], lazy[M], t[M]; void Sort(int ID) { for (int i = L[ID]; i <= R[ID]; i++) t[i] = a[i]; sort(t + L[ID], t + R[ID] + 1); } void build() { len = sqrt(n); //块的长度 tot = n / len; //一共有多少块 if (n % len) tot++; for (int i = 1; i <= n; i++) id[i] = (i - 1) / len + 1; for (int i = 1; i <= tot; i++) { //记录每一块的起点和终点 L[i] = (i - 1) * len + 1; R[i] = i * len; } R[tot] = n; //最后一块不一定有len个,末尾是n for (int i = 1; i <= tot; i++) Sort(i); //块内排序 } void add(int l, int r, ll x) { // 区间加法 int sid = id[l], eid = id[r]; if (sid == eid) { // 在一个块中 for (int i = l; i <= r; i++) a[i] += x; Sort(sid); return; } for (int i = l; i <= R[sid]; i++) a[i] += x; for (int i = sid + 1; i < eid; i++) lazy[i] += x; for (int i = L[eid]; i <= r; i++) a[i] += x; Sort(sid); Sort(eid); } ll query(int l, int r, int x) { // 区间查询 int ans = 0, sid = id[l], eid = id[r]; if (sid == eid) { for (int i = l; i <= r; i++) if (a[i] + lazy[sid] < x) ans++; return ans; } for (int i = l; i <= R[sid]; i++) if (a[i] + lazy[sid] < x) ans++; for (int i = sid + 1; i < eid; i++) ans = ans + (lower_bound(t + L[i], t + R[i] + 1, x - lazy[i]) - t) - L[i]; for (int i = L[eid]; i <= r; i++) if (a[i] + lazy[eid] < x) ans++; return ans; } int main() { CLOSE cin >> n; for (int i = 1; i <= n; i++) cin >> a[i]; build(); for (int i = 1; i <= n; i++) { int op, l, r, c; cin >> op >> l >> r >> c; if (op == 0) add(l, r, c); else cout << query(l, r, c * c) << endl; } return 0; } 数列分块入门3 区间加、区间查询x的前驱 区间修改:区间每个数字都加v 区间查询:区间内x的前驱,也就是区间内小于x的最大的数字。 这个题做法可以和上一个题一样即可,只需要稍微改改二分的过程。 int n, q, tot; int id[M], len, L[M], R[M]; ll a[M], lazy[M], t[M]; void Sort(int ID) { for (int i = L[ID]; i <= R[ID]; i++) t[i] = a[i]; sort(t + L[ID], t + R[ID] + 1); } void build() { len = sqrt(n); //块的长度 tot = n / len; //一共有多少块 if (n % len) tot++; for (int i = 1; i <= n; i++) id[i] = (i - 1) / len + 1; for (int i = 1; i <= tot; i++) { //记录每一块的起点和终点 L[i] = (i - 1) * len + 1; R[i] = i * len; } R[tot] = n; //最后一块不一定有len个,末尾是n for (int i = 1; i <= tot; i++) Sort(i); //块内排序 } void add(int l, int r, ll x) { // 区间加法 int sid = id[l], eid = id[r]; if (sid == eid) { // 在一个块中 for (int i = l; i <= r; i++) a[i] += x; Sort(sid); return; } for (int i = l; i <= R[sid]; i++) a[i] += x; for (int i = sid + 1; i < eid; i++) lazy[i] += x; for (int i = L[eid]; i <= r; i++) a[i] += x; Sort(sid); Sort(eid); } ll query(int l, int r, int x) { // 区间查询 ll ans = -1; int sid = id[l], eid = id[r]; if (sid == eid) { for (int i = l; i <= r; i++) if (a[i] + lazy[sid] < x) ans = max(ans, a[i] + lazy[sid]); return ans; } for (int i = l; i <= R[sid]; i++) if (a[i] + lazy[sid] < x) ans = max(ans, a[i] + lazy[sid]); for (int i = sid + 1; i < eid; i++) { int pos = (lower_bound(t + L[i], t + R[i] + 1, x - lazy[i]) - t); if (--pos < L[i]) continue; ans = max(ans, t[pos] + lazy[i]); } for (int i = L[eid]; i <= r; i++) if (a[i] + lazy[eid] < x) ans = max(ans, a[i] + lazy[eid]); return ans; } int main() { CLOSE cin >> n; for (int i = 1; i <= n; i++) cin >> a[i]; build(); for (int i = 1; i <= n; i++) { int op, l, r, c; cin >> op >> l >> r >> c; if (op == 0) //修改 add(l, r, c); else cout << query(l, r, c) << endl; } return 0; } 启发:可以在整块的维护部分,用其他数据结构维护,如STL的set / multiset ,这样可以很方便的维护插入、删除元素,且代码会更短 数列分块入门5 区间开方、区间求和 区间修改:区间每个数字都开方(下取整) 区间查询:区间求和 类似于势能线段树。 一个数字经过若干次开方之后,会变成1。且次数不会很多。 所以我们考虑,维护整块:这一块内所有数字是否都已经变成0或1了。 整块修改:如果块内存在大于1的数字,那么暴力开方,如果都是0或1,则不需要动 非整块修改:暴力即可。 int id[N], n, len; // id 表示块的编号, len=sqrt(n) , 即上述题解中的s, sqrt的时候时间复杂度最优 int a[N], lazy[N], s[N], L[N], R[N], tot; // a 数组表示数据数组, lazy 数组记录每个块的整体赋值情况, 类似于 lazy_tag, s表示块内元素总和 void build() { len = sqrt(n); //块的长度 tot = n / len; //一共有多少块 if (n % len) tot++; for (int i = 1; i <= n; i++) { id[i] = (i - 1) / len + 1; s[id[i]] += a[i]; } for (int i = 1; i <= tot; i++) { //记录每一块的起点和终点 L[i] = (i - 1) * len + 1; R[i] = i * len; } R[tot] = n; //最后一块不一定有len个,末尾是n } void update(int l, int r, int x) { // 区间开根号,一个数字多次开根号,很快就会变为0/1 int sid = id[l], eid = id[r]; if (sid == eid) { // 在一个块中 for (int i = l; i <= r; i++) { s[sid] -= (a[i] - sqrt(a[i])); a[i] = sqrt(a[i]); } return; } for (int i = l; id[i] == sid; i++) { s[sid] -= (a[i] - sqrt(a[i])); a[i] = sqrt(a[i]); } for (int i = sid + 1; i < eid; i++) { if (lazy[i] == 1) continue; //如果这一块全是0/1,可以直接跳过 int cnt = 0; for (int j = L[i]; j <= R[i]; j++) { s[i] -= (a[j] - sqrt(a[j])); a[j] = sqrt(a[j]); if (a[j] <= 1) cnt++; } if (cnt == R[i] - L[i] + 1) lazy[i] = 1; } for (int i = r; id[i] == eid; i--) { s[eid] -= (a[i] - sqrt(a[i])); a[i] = sqrt(a[i]); } } ll query(int l, int r) { // 区间查询 int sid = id[l], eid = id[r]; ll ans = 0; if (sid == eid) { // 在一个块里直接暴力求和 for (int i = l; i <= r; i++) ans = (ans + a[i]); return ans; } for (int i = l; id[i] == sid; i++) ans = (ans + a[i]); for (int i = sid + 1; i < eid; i++) ans = (ans + s[i]); for (int i = r; id[i] == eid; i--) ans = (ans + a[i]); return ans; } int main() { // CLOSE cin >> n; for (int i = 1; i <= n; i++) cin >> a[i]; build(); for (int i = 1; i <= n; i++) { int op, l, r, c; cin >> op >> l >> r >> c; if (op == 0) update(l, r, c); else cout << query(l, r) << endl; } return 0; } 数列分块入门8 区间修改为c,区间查询等于c的数字个数 区间修改比较简单 麻烦在区间查询比较奇怪,因为权值种类比较多, 好在要求对于[l,r]查询之后,还要将区间都修改为c。 所以我们会发现:经过几轮查询/修改之后,可能就只剩下几段不同的区间了,类似于上一个开根号的题。 所以我们考虑:维护一个块内,是否只有一种数字。 数列分块入门6 单点插入、单点查询 单点插入:在第x个数字之前插入一个数字v 单点查询:查询第x个数字是多少 注意:数据随机生成。 每一块用一个vector维护。 插入操作:先整块遍历,找到要插入到第几块中,然后直接insert即可。 查询:先整块遍历,找到查询的数字在第几块中,然后直接输出即可。 int a[N], n, len, tot; vector<int> ve[N]; void build() { len = sqrt(n); tot = n / len; //一共有多少块 if (n % len) tot++; for (int i = 1; i <= n; i++) ve[(i - 1) / len + 1].push_back(a[i]); } void update(int p, int c) { for (int i = 1; i <= tot; i++) { if (ve[i].size() < p) { p -= ve[i].size(); continue; } ve[i].insert(ve[i].begin() + p - 1, c); break; } } int query(int p) { int ans; for (int i = 1; i <= tot; i++) { if (ve[i].size() < p) { p -= ve[i].size(); continue; } ans = ve[i][p - 1]; break; } return ans; } int main() { CLOSE cin >> n; for (int i = 1; i <= n; i++) cin >> a[i]; build(); for (int i = 1; i <= n; i++) { int op, l, r, c; cin >> op >> l >> r >> c; if (op == 0) update(l, r); else cout << query(r) << endl; } return 0; } 用vector可以水很多分,N开2e5能水70分,N开1e5+5 能水过。 vector<int> ve(N); int n; int main() { CLOSE cin >> n; for (int i = 1; i <= n; i++) { int x; cin >> x; ve[i] = x; } for (int i = 1; i <= n; i++) { int op, l, r, c; cin >> op >> l >> r >> c; if (op == 0) ve.insert(ve.begin() + l, r); else cout << ve[r] << endl; } return 0; } ps:如果数据不随机呢? 如果在同一块内,进行大量的单点插入,那么这个块的大小就会非常大,那么这一块的暴力就没法保证时间复杂度了。 那么可以怎么办呢? 重构,重新分块即可。 每经过$\sqrt n$ 次插入操作之后,进行一次重新分块。这样可以保证每块的大小比较均匀 int n, a[N], op, l, r, c, len, tot, cnt; vector<int> ve[N]; void build() { int cnt = 0; for (int i = 1; i <= tot; ++i) { for (int j = 0; j < ve[i].size(); ++j) a[++cnt] = ve[i][j]; //最开始ve是空的不会执行 ve[i].clear(); } len = sqrt(n); tot = n / len; if (n % len) ++tot; for (int i = 1; i <= n; ++i) ve[(i - 1) / len + 1].push_back(a[i]); } void update(int p, int c) { for (int i = 1; i <= tot; ++i) { if (ve[i].size() < p) { p -= ve[i].size(); continue; } ve[i].insert(ve[i].begin() + p - 1, c); break; } } int query(int p) { for (int i = 1; i <= tot; ++i) { if (ve[i].size() < p) { p -= ve[i].size(); continue; } return ve[i][p - 1]; } } int main() { CLOSE cin >> n; for (int i = 1; i <= n; ++i) cin >> a[i]; build(); int m = n; for (int i = 1; i <= m; ++i) { cin >> op >> l >> r >> c; if (op == 0) { ++n; if (cnt >= (int)sqrt(n)) { cnt = 0; build(); } update(l, r); ++cnt; } else cout << query(r) << endl; } return 0; } 1 个帖子 - 1 位参与者 阅读完整话题

linux.do · 2026-04-13 11:02:25+08:00 · tech

手上有个机器,配置如图 目前主要是想用来学习一些入门的AI知识,例如本地文本模型,语音,文生图,文生视频,还有 CompyUi 想要咨询一下专业大佬,是建议我升级一下好啊还是卖了直接换新的比较好 我现在知道的要升级内存,D4的3600频率的是否够用了,还有这个显卡准备上入门的5060ti16g, 只是用来学习用是否够用了,或者我看有人推荐3090ti,不知道是否合适,求佬友指点 对了,这个是不是可以直接加一块固态硬盘 谢谢 1 个帖子 - 1 位参与者 阅读完整话题