首页 文章 精选 留言 我的

精选列表

搜索[star],共5415篇文章
优秀的个人博客,低调大师

2.9k Star—Comet 0.4.0-rc.1正式发布啦

## Comet 0.4.0的第一个生产候选(0.4.0-rc.1)版本正式发布了 经历了2个月的开发,20个beta版本迭代,共计24名开发者参与 在本次RC版本中,Comet完成了Dashboard性能优化,新增个人记忆、项目知识的Agent Learning Loop闭环 同时带来了更加强大的Supervisor Change特性,现在你能够在Codex或Claude Code中通过多会话/Agent Teams执行Native工作流, 用Supervisor Change架构管理复杂的大型需求 Feature: 1. Dashboard加入了SQLite持久化数据,现在你不仅可以在Dashboard上观测单Change/Supervisor Change的流程,还可以观测到学习到的个人记忆、项目知识 2. 个人记忆、项目知识自进化插件加入,现在Comet Native和Comet Classic都可以在用户使用过程中学习记忆及项目知识 3. Native Verify性能优化,降低总体22%耗时 4. 更多的平台支持,Oh My Pi、Deepseek Harness原生支持 5. 更加流畅的Native Skill交付流程,更多的归档选项 6. 更加适合阅读的Website,更加吸引人的Native文章,现在你能够在首页进行dashboard交互,查看Claude Code/Codex的多Agent演示视频 RC1详细评测见https://docs.comet.rpamis.com/zh/eval/comet-native-vs-rc1-beta16-experiment 其余完整变更见changelog https://docs.comet.rpamis.com/zh/changelog#0-4-0-rc-1 以下是实机特性演示 Supervisor Change: 用一个根需求管理所有子需求,自动并发推进,支持在Codex中多会话推进、在Claude Code中采用Agent Teams推进,视频可见文档站官网,[Comet - Comet Docs](https://docs.comet.rpamis.com/zh/home) ![558b58a78f8035ee7b4457c734b61ac8.jpeg](https://developer.qcloudimg.com/http-save/yehe-2832944/608600c5a1a66c876d0cd56d2701a376.png) ![42ff0c71d0e0417fa68b3e63cbee8eb8.jpeg](https://developer.qcloudimg.com/http-save/yehe-2832944/e0935aa5de67197a5f141343695cc689.png) 相关文章 1: Comet 不止Skill https://docs.comet.rpamis.com/zh/tech-blog/comet-beyond-skill 2: Comet与业界实践的对比 https://docs.comet.rpamis.com/zh/tech-blog/comet-vs-industry

优秀的个人博客,低调大师

微软近 5 万 star 的开源项目 —— MarkItDown 已支持 MCP

MarkItDown 是微软开源的 Python 实用工具库,支持将各种文件转换为 Markdown 格式,适用于索引、文本分析等用途。 MarkItDown 目前支持以下文件: PDF PowerPoint Word Excel 图片(EXIF 元数据和 OCR) 音频(EXIF 元数据和语音转录) HTML 基于文本的格式(CSV、JSON、XML) ZIP 文件(遍历内容) 该项目最近发布了一项“史诗级”更新 —— 支持 MCP。MarkItDown 现已提供 MCP(模型上下文协议)服务器 (MarkItDown-MCP),以便与 LLM 应用程序如 Claude Desktop 集成。 MarkItDown-MCP 提供两种主要的服务器模式: STDIO模式(默认):通过标准输入/输出进行通信,非常适合与命令行工具和脚本集成。 SSE模式:作为服务器发送事件(Server-Sent Events)服务器在指定主机和端口上运行,支持基于 Web 和网络的集成。 Docker 支持 为了增强可移植性和隔离性,MarkItDown-MCP 提供了 Docker 支持。这在以下情况特别有用: 确保在不同系统上的环境一致性 将转换过程与主机系统隔离 与 Claude Desktop 等远程服务协作 Docker 集成包括挂载本地目录的功能,允许容器访问和转换本地文件,同时维持安全边界。 更多信息查看markitdown-mcp。

优秀的个人博客,低调大师

跟着 8.6k Star 的开源数据库,搞 RAG!

过去 9 年里,HelloGitHub 月刊累计收录了 3000 多个开源项目。然而,随着项目数量的增加,不少用户反馈:"搜索功能不好用,找不到想要的项目!" 这让我意识到,仅仅收录项目是不够的,还需要通过更智能的方式,帮助用户找到心仪的开源项目。于是,我开始探索如何通过 RAG 技术解决这个问题。 检索增强生成(RAG),是赋予生成式人工智能模型信息检索能力的技术。 RAG 技术我早有耳闻,但却一直不知道该从哪里入手。虽然现在有不少容易上手的 RAG 低代码平台,但我不想只停留在"会用"的层面,更希望了解它的实现细节,否则不敢在生产环境中用。不过,要让我直接用 LangChain 和 Ollama 从零搭建一个 RAG 系统,还真有点心里没底。 还好最近 OceanBase 搞事情,在 4.3.3 版本里支持了向量检索功能,更贴心的是,还专门为像我这样对 RAG 感兴趣的新手,准备了一个用 Python 搭建 RAG 聊天机器人的实战教程。 GitHub 地址:github.com/oceanbase/oceanbase 光看永远只是纸上谈兵,所以我干脆上手把玩了一番。 接下来,我将分享如何基于该项目,打造一款 HelloGitHub 开源社区的聊天机器人,内容包括实现过程、细节优化,以及对 RAG 技术的理解与未来展望。 一、介绍 OceanBase 开源的 RAG 聊天机器人,能够通过自然对话更精准地回答与 OceanBase 文档相关的问题。 该项目是基于 langchain、langchain-oceanbase 和 streamlit 构建,处理流程是先将 OceanBase 数据库的文档,通过 Embedding 模型转化为向量数据,并存储在 OceanBase 数据库中。当用户提问时,系统会用相同的模型将问题转化为向量,然后通过向量检索找到相关的文档内容,再将这些文档作为上下文提交给大语言模型,从而生成更精准的回答。 在线体验:oceanbase.com/obi 体验后,我感觉效果还不错,于是就萌生了一个想法:能不能把 OceanBase 的文档,换成 HelloGitHub 月刊的 Markdown 文件,灌进系统里,这样不就摇身一变,成为 HelloGitHub 专属的聊天机器人了吗? 说干就干! 二、安装运行 在开始改造之前,首先需要把项目跑起来。安装运行的步骤在 OceanBase 提供的实战教程中已经很详细了,这里不再过多介绍。运行步骤如下: 执行 embed_docs.py 脚本,将文档内容向量化后存储到 OB 启动项目 streamlit run --server.runOnSave false chat_ui.py 启动成功将自动跳转至此界面: 建议: Python 版本管理:运行需要 Python 3.9+,建议使用 pyenv 管理项目的 Python 版本。 查看数据库:不论是通过 Docker 部署 OceanBase 还是使用 OB Cloud,都建议在本地通过 GUI 工具查看数据库,有助于开发和调试。 运行 embed_docs.py 脚本后,查看数据库中的表,你会发现这些字段: document:存储原始的文档内容 embedding:存储文档向量化后的数据 metadata:记录文档的名称、路径以及切分后的标题等信息 其中 embedding 列是一个类似数组形式的数据,这个就是通过 Embedding 模型将文档片段转化为向量数据的结果。这些向量数据能够捕捉文本的语义信息,使计算机能够更好地理解文本的含义,从而实现类似语义搜索的功能(计算距离),为后续问题与文档内容的匹配提供基础。 三、动手改造 这个项目除了支持 LLMs API,还可以切换为本地的 Ollama API 使用,只需修改 .env 配置文件即可完成调整: # 使用支持 embed API 的模型 OLLAMA_URL=localhost:11434/api/embed OLLAMA_TOKEN= OLLAMA_MODEL=all-minilm 注意:在调用第三方付费 API 时,一定要注意使用量,建议仅导入部分文档用于测试,或用本地 LLM 调试逻辑,避免不必要的花费。 3.1 导入 HelloGitHub 月刊 通过 embed_docs.py 脚本,将 HelloGitHub 月刊内容向量化并导入到 OceanBase 数据库,命令如下: python embed_docs.py --doc_base /HelloGitHub/content --table_name hg 参数说明: doc_base:HelloGitHub 内容目录 table_name:脚本会自动创建该表,并将数据存储到表中。 但是运行后,我查看数据库时发现 document 字段中包含了许多无意义的内容,例如格式符号或无关信息: 面对这些噪声数据,我编写了一个脚本,清洗 HelloGitHub 月刊文件中无关的格式符号和冗余内容,并重新导入数据库。 3.2 启动服务 在启动服务时,需要通过环境变量 TABLE_NAME 指定要使用的表。命令如下: TABLE_NAME=hg2 streamlit run --server.runOnSave false chat_ui.py 我试了一下,回答的效果并不理想: 经过测试,我分析问答效果欠佳的原因,可能包括以下几点: 向量化效果:所选用的模型 all-minilm 仅有 384 维度,可以尝试更大的 Embedding 模型; 数据清理:虽然清理了一部分无用内容,但可能还有一些噪声数据未处理完全; 文档完整性:HelloGitHub 的内容结构是否适合问答模型需要进一步分析; 提示词:需要完善提示词设计,补充更多上下文; 四、优化问答效果 我开始对 RAG 有些感觉了,所以准备切换到付费但效果更好的通义千问 text-embedding-v3 模型(1024 维度),进行调试。 4.1 数据优化 为提升问答效果,我决定进一步优化 document 的构造方式。具体思路是:将 HelloGitHub 网站中的表导入至 OceanBase 数据库,并基于这些表的数据,构建更干净和精准的内容。这样可以最大程度地确保项目数据的全面性,同时减少无关内容的干扰,提升向量检索相关性。 导入表到 OceanBase OceanBase 和 MySQL 高度兼容,因此,我直接用 Navicat 将 HelloGitHub 的数据表结构和内容,从 MySQL 无缝迁移到了 OceanBase。然后我写了一个 embed_sql.py 脚本,通过直接查询相关表的数据,进而生成更精简的内容(document),同时补充元数据(metadata),并存储到数据库。核心代码如下: # 构建内容(document) content = f"""{row.get('name', '未知')}:{row.get('title', '未知标题')}。{row.get('summary', '暂无概要')}""" # 构建元数据(metadata) metadata = { "repository_name": row.get("name", "N/A"), # 仓库名称 "repository_url": row.get("url", "N/A"), # 仓库链接 "description": row.get("summary", "N/A"), # 项目描述 "category_name": row.get("category_name", "N/A"), # 类别名称 "language": row.get("primary_lang", "N/A"), # 主要编程语言 "chunk_title": row.get("name", "N/A"), "enhanced_title": f'内容 -> {row.get("category_name", "N/A")} -> {row.get("name", "N/A")}' ... } # 将内容和元数据添加到文档对象 docs.append(Document(page_content=content.strip(), metadata=metadata)) # 存储到数据库 vs.add_documents( docs, ids=[str(uuid.uuid4()) for _ in range(len(docs))], ) 经过多轮调试和对比,我发现 document 数据越精简,向量检索效果越好 ,随后将完整的数据集存入 OceanBase 数据库的 hg5 表。 python embed_sql.py --table_name hg5 --limit=4000 args Namespace(table_name='hg5', batch_size=4, limit=4000, echo=False) Using RemoteOpenAI Processing: 100%|███████████████████████████████████████████████████████████████████████▉| 3356/3357 [09:33<00:00, 5.85row/s] 至此,基于数据库表构造的 document 数据,已经非常干净了。 4.2 提示词优化 在优化完数据后,我开始思考如何优化提示词,并对 LLM 的回答进行引导和强化。以下是针对 LLM 提示词优化的方向: 明确背景和任务:在提示词中设定问答的背景并限制问题的范围,例如,确保问题只涉及开源项目或 HelloGitHub 的内容。 丰富上下文:将 metadata(元数据) 和 document(项目描述)同时提供给大模型,让 LLM 有更多上下文来生成精确回答。 高质量示例:提供高质量的回答示例,统一输出格式。 约束逻辑:明确要求 LLM 不得虚构答案。如无法回答问题,需清楚指出知识盲点,并合理提供方向性建议。 4.3 处理流程优化 在优化向量检索和回答的流程方面,我做了以下改进: 扩大检索范围:向量检索默认只返回前 10 条最高相似度的内容。我将其扩展至 20 条,为 LLM 提供更多上下文选择。 判断相关性:使用提示词指导 LLM 在输出答案前,先判断问题是否与 HelloGitHub 或开源项目相关,避免生成无关回答。 提炼回答:基于用户输入分析意图后,选出最相关的 5 个项目,并结合元数据生成更贴合用户需求的回答。 4.4 效果展示 除了上面的优化,我还进一步简化了页面、删除用不到的代码,最终呈现效果如下: 回答效果对比: 通过切换至通义千问 text-embedding-v3 模型,同时优化数据、提示词策略和问答流程,让这套 RAG 系统的回答质量有了明显提升,我打算自己盘一盘再上线。所以先放出源码,感兴趣的小伙伴可以作为参考: GitHub 地址:github.com/521xueweihan/ai-workshop-2024 五、最后 在构建 HelloGitHub 的 RAG 聊天机器人过程中,回答效果一直不好,让我一度产生了放弃的念头。但当我通过查询表里的数据构造 document,并使用维度更大的 Embeding 模型后,回答效果直线提升,才让我重新看到了希望。 这段经历也让我开始认真思考:优化 RAG 的关键是什么?我的答案是 数据+检索 。如今,许多企业希望借助 AI 技术赋能已有服务,RAG 则是一种门槛较低的通用解决方案。在这一过程中,数据质量决定了基础,高质量数据往往是从海量数据提纯而来。检索则是确保内容能够被快速且准确提取的关键。否则不管提示词再怎么优化,也无法检索到有价值的内容,就无法实现增强的效果。 另外,我认为在未来的 RAG 应用中,除了向量数据,数据库还需要具备一些关键能力来确保检索和生成的高效性。例如,支持关系型数据和向量数据的混合搜索,不仅能处理结构化和非结构化数据,还能有效减少 RAG 模型中的"幻觉"问题,从而让生成的答案更准确、更有根据。图搜索(知识图谱)同样很重要,它为 RAG 提供复杂推理所需的背景信息,提升生成质量。此外,RAG 应用在许多场景中需要频繁更新和同步数据,因此数据库还需支持实时查询、低延迟响应、事务处理和高可用性,这些是确保 RAG 高效运行的基础。 OceanBase 的分布式架构优势,让它在面对海量数据时依然游刃有余。而新引入的向量存储和检索能力,使得我们能够通过 SQL 轻松获取最"干净"的数据,并在同一个数据库内完成向量化操作。OceanBase 未来可期! GitHub 地址:github.com/oceanbase/oceanbase 虽然 RAG 技术目前还不像 Web 开发那么成熟,但作为一个潜力巨大的技术方向,值得我们持续关注和学习。作为一名刚刚踏上 RAG 探索之旅的小白,我还有很多需要学习的地方。如果你也对 RAG 充满兴趣,欢迎结伴而行、共同成长!

优秀的个人博客,低调大师

1.3k Star!免费部署私人聊天工具—VoceChat

随着网络的广泛应用,数据安全和隐私保护变得非常重要,已经成为我们日常生活中不可或缺的一部分。在这个信息时代,我们不仅需要享受网络带来的便利,更要关注个人数据的安全性,让数据完全由用户自己掌握。 应用简览 VoceChat 是一款支持独立部署的安全聊天软件。是当今市场上最小、最稳定、最高效的独立聊天服务器支持全程加密传输,读取后可立即刻录。 VoceChat 适用于小型团队内部交流、网站嵌入、私人聊天等场景。 主要功能 支持Markdown格式: 可以在聊天中使用Markdown格式,以便更丰富地编辑和分享文本内容。 数据安全: 系统注重数据安全,采取相应的措施来确保用户的数据在传输和存储过程中得到充分的保护。 支持中英文: 提供中英文双语支持,以满足不同用户的语言需求。 支持第三方登录: 用户可以通过第三方登录方式(如Google、GitHub等)进行账户登录,提高登录的便捷性和选择性。 邀请用户: 用户可以通过应用内的邀请功能,邀请其他用户加入VoceChat,扩大用户社交圈或团队。 支持移动端: VoceChat提供移动端应用,用户可以在手机或其他移动设备上使用应用,方便随时随地进行聊天和互动。 文件传输: 用户可以方便地在VoceChat中进行文件传输,包括发送和接收各种类型的文件,如照片、文档、音频等。 收藏管理: 用户可以使用收藏管理功能,将重要的消息、文件或链接保存在收藏夹中,方便随时查看和整理。 应用特色 一、链接邀请 提供了简便的邀请功能,通过邀请链接或其他方式,能轻松邀请朋友或团队成员加入。 二、设置第三方登录 通过设置第三方登录,VoceChat实现了便捷的账户登录方式,用户可使用已有账户,简化登录流程,提高用户体验。 三、支持移动端 VoceChat在移动端上提供了全面支持,用户可以随时随地使用应用,保障了流畅的移动使用体验。 四、Markdown格式聊天 支持Markdown格式聊天使用户能够以更富表现力的方式进行沟通,创造更丰富多彩的聊天体验。 五、收藏管理 收藏管理功能让用户轻松保存和整理重要信息,提高工作和沟通效率,方便快捷地查看和管理已收藏的内容。 安装指南 进入云原生应用市场 搜索 VoceChat 进入详情,选择包类型(本应用支持,docker安装,ram安装) 点击安装,执行相应命令即可。如有疑问可参阅使用文档或加入社区 关于云原生应用市场 云原生应用市场是一个汇聚了各类开源软件的应用市场,不仅可以作为你自己的 Helm Chart 仓库,提供丰富多样的Helm应用,还有 Docker 应用、Rainbond 应用模板、信创应用等多种选择。 官网:https://hub.grapps.cn/ 微信群:关注 云原生应用市场 公众号加入技术交流群

优秀的个人博客,低调大师

技术干货 | 源码解析 Github 上 14.1k Star 的 RocketMQ

站在发送方视角,通过源码,来分析在事务消息发送中 RocketMQ 是如何工作的。 前言 Apache RocketMQ 作为广为人知的开源消息中间件,诞生于阿里巴巴,于 2016 年捐赠给了 Apache。从 RocketMQ 4.0 到如今最新的 v4.7.1,不论是在阿里巴巴内部还是外部社区,都赢得了广泛的关注和好评。 本文将站在发送方视角,通过阅读 RocketMQ Producer 源码,来分析在事务消息发送中 RocketMQ 是如何工作的。 需要说明的是,本文所贴代码,均来自 4.7.1 版本的 RocketMQ 源码。本文中所讨论的发送,仅指从 Producer 发送到 Broker 的过程,并不包含 Broker 将消息投递到 Consumer 的过程。宏观概览 RocketMQ 事务消息发送流程: 结合源码来看,RocketMQ 的事务消息 TransactionMQProducer 的 sendMessageInTransaction 方法,实际调用了 DefaultMQProducerImpl 的 sendMessageInTransaction 方法。我们进入 sendMessageInTransaction 方法,整个事务消息的发送流程清晰可见。 首先,做发送前检查,并填入必要参数,包括设 prepare 事务消息。 源码清单-1 publicTransactionSendResultsendMessageInTransaction(finalMessagemsg, finalLocalTransactionExecuterlocalTransactionExecuter,finalObjectarg) throwsMQClientException{ TransactionListenertransactionListener=getCheckListener(); if(null==localTransactionExecuter&&null==transactionListener){ thrownewMQClientException("tranExecutorisnull",null); } //ignoreDelayTimeLevelparameter if(msg.getDelayTimeLevel()!=0){ MessageAccessor.clearProperty(msg,MessageConst.PROPERTY_DELAY_TIME_LEVEL); } Validators.checkMessage(msg,this.defaultMQProducer); SendResultsendResult=null; MessageAccessor.putProperty(msg,MessageConst.PROPERTY_TRANSACTION_PREPARED,"true"); MessageAccessor.putProperty(msg,MessageConst.PROPERTY_PRODUCER_GROUP,this.defaultMQProducer.getProducerGroup()); 进入发送处理流程: 源码清单-2 try{ sendResult=this.send(msg); }catch(Exceptione){ thrownewMQClientException("sendmessageException",e); } 根据 broker 返回的处理结果决策本地事务是否执行,半消息发送成功则开始本地事务执行: 源码清单-3 LocalTransactionStatelocalTransactionState=LocalTransactionState.UNKNOW; ThrowablelocalException=null; switch(sendResult.getSendStatus()){ caseSEND_OK:{ try{ if(sendResult.getTransactionId()!=null){ msg.putUserProperty("__transactionId__",sendResult.getTransactionId()); } StringtransactionId=msg.getProperty(MessageConst.PROPERTY_UNIQ_CLIENT_MESSAGE_ID_KEYIDX); if(null!=transactionId&&!"".equals(transactionId)){ msg.setTransactionId(transactionId); } if(null!=localTransactionExecuter){ localTransactionState=localTransactionExecuter.executeLocalTransactionBranch(msg,arg); }elseif(transactionListener!=null){ log.debug("UsednewtransactionAPI"); localTransactionState=transactionListener.executeLocalTransaction(msg,arg); } if(null==localTransactionState){ localTransactionState=LocalTransactionState.UNKNOW; } if(localTransactionState!=LocalTransactionState.COMMIT_MESSAGE){ log.info("executeLocalTransactionBranchreturn{}",localTransactionState); log.info(msg.toString()); } }catch(Throwablee){ log.info("executeLocalTransactionBranchexception",e); log.info(msg.toString()); localException=e; } } break; caseFLUSH_DISK_TIMEOUT: caseFLUSH_SLAVE_TIMEOUT: caseSLAVE_NOT_AVAILABLE://当备broker状态不可用时,半消息要回滚,不执行本地事务 localTransactionState=LocalTransactionState.ROLLBACK_MESSAGE; break; default: break; } 本地事务执行结束,根据本地事务状态进行二阶段处理: 源码清单-4 try{ this.endTransaction(sendResult,localTransactionState,localException); }catch(Exceptione){ log.warn("localtransactionexecute"+localTransactionState+",butendbrokertransactionfailed",e); } //组装发送结果 //... returntransactionSendResult; } 接下来,我们深入每个阶段代码分析。深扒内幕 Ⅰ阶段发送 重点分析 send 方法。进入 send 方法后,我们发现,RocketMQ 的事务消息的一阶段,使用了 SYNC 同步模式: 源码清单-5 publicSendResultsend(Messagemsg, longtimeout)throwsMQClientException,RemotingException,MQBrokerException,InterruptedException{ returnthis.sendDefaultImpl(msg,CommunicationMode.SYNC,null,timeout); } 这一点很容易理解,毕竟事务消息是要根据一阶段发送结果来决定要不要执行本地事务的,所以一定要阻塞等待 broker 的 ack。 我们进入 DefaultMQProducerImpl.java 中去看 sendDefaultImpl 方法的实现,通过读这个方法的代码,来尝试了解在事务消息的一阶段发送过程中 producer 的行为。 值得注意的是,这个方法并非为事务消息定制,甚至不是为 SYNC 同步模式定制的,因此读懂了这段代码,基本可以对 RocketMQ 的消息发送机制有了一个较为全面的认识。 这段代码逻辑非常通畅,不忍切片。为了节省篇幅,将代码中较为繁杂但信息量不大的部分以注释代替,尽可能保留流程的完整性。个人认为较为重要或是容易被忽略的部分,以注释标出,后文还有部分细节的详细解读。 源码清单-6 privateSendResultsendDefaultImpl( Messagemsg, finalCommunicationModecommunicationMode, finalSendCallbacksendCallback, finallongtimeout )throwsMQClientException,RemotingException,MQBrokerException,InterruptedException{ this.makeSureStateOK(); //一、消息有效性校验。见后文 Validators.checkMessage(msg,this.defaultMQProducer); finallonginvokeID=random.nextLong(); longbeginTimestampFirst=System.currentTimeMillis(); longbeginTimestampPrev=beginTimestampFirst; longendTimestamp=beginTimestampFirst; //获取当前topic的发送路由信息,主要是要broker,如果没找到则从namesrv获取 TopicPublishInfotopicPublishInfo=this.tryToFindTopicPublishInfo(msg.getTopic()); if(topicPublishInfo!=null&&topicPublishInfo.ok()){ booleancallTimeout=false; MessageQueuemq=null; Exceptionexception=null; SendResultsendResult=null; //二、发送重试机制。见后文 inttimesTotal=communicationMode==CommunicationMode.SYNC?1+this.defaultMQProducer.getRetryTimesWhenSendFailed():1; inttimes=0; String[]brokersSent=newString[timesTotal]; for(;times<timesTotal;times++){ //第一次发送是mq==null,之后都是有broker信息的 StringlastBrokerName=null==mq?null:mq.getBrokerName(); //三、rocketmq发送消息时如何选择队列?——broker异常规避机制 MessageQueuemqSelected=this.selectOneMessageQueue(topicPublishInfo,lastBrokerName); if(mqSelected!=null){ mq=mqSelected; brokersSent[times]=mq.getBrokerName(); try{ beginTimestampPrev=System.currentTimeMillis(); if(times>0){ //Resettopicwithnamespaceduringresend. msg.setTopic(this.defaultMQProducer.withNamespace(msg.getTopic())); } longcostTime=beginTimestampPrev-beginTimestampFirst; if(timeout<costTime){ callTimeout=true; break; } //发送核心代码 sendResult=this.sendKernelImpl(msg,mq,communicationMode,sendCallback,topicPublishInfo,timeout-costTime); endTimestamp=System.currentTimeMillis(); //rocketmq选择broker时的规避机制,开启sendLatencyFaultEnable==true才生效 this.updateFaultItem(mq.getBrokerName(),endTimestamp-beginTimestampPrev,false); switch(communicationMode){ //四、RocketMQ的三种CommunicationMode。见后文 caseASYNC://异步模式 returnnull; caseONEWAY://单向模式 returnnull; caseSYNC://同步模式 if(sendResult.getSendStatus()!=SendStatus.SEND_OK){ if(this.defaultMQProducer.isRetryAnotherBrokerWhenNotStoreOK()){ continue; } } returnsendResult; default: break; } }catch(RemotingExceptione){ //... //自动重试 }catch(MQClientExceptione){ //... //自动重试 }catch(MQBrokerExceptione){ //... //仅返回码==NOT_IN_CURRENT_UNIT==205时自动重试 //其他情况不重试,抛异常 }catch(InterruptedExceptione){ //... //不重试,抛异常 } }else{ break; } } if(sendResult!=null){ returnsendResult; } //组装返回的info信息,最后以MQClientException抛出 //...... //超时场景抛RemotingTooMuchRequestException if(callTimeout){ thrownewRemotingTooMuchRequestException("sendDefaultImplcalltimeout"); } //填充MQClientException异常信息 //... } validateNameServerSetting(); thrownewMQClientException("Norouteinfoofthistopic:"+msg.getTopic()+FAQUrl.suggestTodo(FAQUrl.NO_TOPIC_ROUTE_INFO), null).setResponseCode(ClientErrorCode.NOT_FOUND_TOPIC_EXCEPTION); } 一、消息有效性校验 源码清单-7 Validators.checkMessage(msg,this.defaultMQProducer); 在此方法中校验消息的有效性,包括对 topic 和消息体的校验。topic 的命名必须符合规范,且避免使用内置的系统消息 TOPIC。消息体长度 > 0 && 消息体长度 <= 102410244 = 4M 。 源码清单-8 publicstaticvoidcheckMessage(Messagemsg,DefaultMQProducerdefaultMQProducer) throwsMQClientException{ if(null==msg){ thrownewMQClientException(ResponseCode.MESSAGE_ILLEGAL,"themessageisnull"); } //topic Validators.checkTopic(msg.getTopic()); Validators.isNotAllowedSendTopic(msg.getTopic()); //body if(null==msg.getBody()){ thrownewMQClientException(ResponseCode.MESSAGE_ILLEGAL,"themessagebodyisnull"); } if(0==msg.getBody().length){ thrownewMQClientException(ResponseCode.MESSAGE_ILLEGAL,"themessagebodylengthiszero"); } if(msg.getBody().length>defaultMQProducer.getMaxMessageSize()){ thrownewMQClientException(ResponseCode.MESSAGE_ILLEGAL, "themessagebodysizeovermaxvalue,MAX:"+defaultMQProducer.getMaxMessageSize()); } } 二、发送重试机制 Producer 在消息发送不成功时,会自动重试,最多发送次数 = retryTimesWhenSendFailed + 1 = 3 次 。 值得注意的是,并非所有异常情况都会重试,从以上源码中可以提取到的信息告诉我们,在以下三种情况下,会自动重试: 发生 RemotingException,MQClientException 两种异常之一时 发生 MQBrokerException 异常,且 ResponseCode 是 NOT_IN_CURRENT_UNIT = 205 时 SYNC 模式下,未发生异常且发送结果状态非 SEND_OK 在每次发送消息之前,会先检查是否在前面这两步就已经耗时超长(超时时长默认 3000ms),若是,则不再继续发送并且直接返回超时,不再重试。这里说明了 2 个问题: producer 内部自动重试对业务应用而言是无感知的,应用看到的发送耗时是包含所有重试的耗时在内的; 一旦超时意味着本次消息发送已经以失败告终,原因是超时。这个信息最后会以 RemotingTooMuchRequestException 的形式抛出。 这里需要指出的是,在 RocketMQ 官方文档中指出,发送超时时长是 10s,即 10000ms,网上许多人对 rocketMQ 的超时时间解读也认为是 10s。然而代码中却明明白白写着 3000ms,最终我 debug 之后确认,默认超时时间确实是 3000ms。 三、broker 的异常规避机制 源码清单-9 MessageQueuemqSelected=this.selectOneMessageQueue(topicPublishInfo,lastBrokerName); 这行代码是发送前选择 queue 的过程。 这里涉及 RocketMQ 消息发送高可用的的一个核心机制,latencyFaultTolerance。这个机制是 Producer 负载均衡的一部分,通过 sendLatencyFaultEnable 的值来控制,默认是 false 关闭状态,不启动 broker 故障延迟机制,值为 true 时启用 broker 故障延迟机制,可由 Producer 主动打开。 选择队列时,开启异常规避机制,则根据 broker 的工作状态避免选择当前状态不佳的 broker 代理,不健康的 broker 会在一段时间内被规避,不开启异常规避机制时,则按顺序选取下一个队列,但在重试场景下会尽量选择不同于上次发送 broker 的 queue。每次消息发送都会通过 updateFaultItem 方法来维护 broker 的状态信息。 源码清单-10 publicvoidupdateFaultItem(finalStringbrokerName,finallongcurrentLatency,booleanisolation){ if(this.sendLatencyFaultEnable){ //计算延迟多久,isolation表示是否需要隔离该broker,若是,则从30s往前找第一个比30s小的延迟值,再按下标判断规避的周期,若30s,则是10min规避; //否则,按上一次发送耗时来决定规避时长; longduration=computeNotAvailableDuration(isolation?30000:currentLatency); this.latencyFaultTolerance.updateFaultItem(brokerName,currentLatency,duration); } } 深入到 selectOneMessageQueue 方法内部一探究竟: 源码清单-11 publicMessageQueueselectOneMessageQueue(finalTopicPublishInfotpInfo,finalStringlastBrokerName){ if(this.sendLatencyFaultEnable){ //开启异常规避 try{ intindex=tpInfo.getSendWhichQueue().getAndIncrement(); for(inti=0;i<tpInfo.getMessageQueueList().size();i++){ intpos=Math.abs(index++)%tpInfo.getMessageQueueList().size(); if(pos<0) pos=0; //按顺序取下一个messagequeue作为发送的queue MessageQueuemq=tpInfo.getMessageQueueList().get(pos); //当前queue所在的broker可用,且与上一个queue的broker相同, //或者第一次发送,则使用这个queue if(latencyFaultTolerance.isAvailable(mq.getBrokerName())){ if(null==lastBrokerName||mq.getBrokerName().equals(lastBrokerName)) returnmq; } } finalStringnotBestBroker=latencyFaultTolerance.pickOneAtLeast(); intwriteQueueNums=tpInfo.getQueueIdByBroker(notBestBroker); if(writeQueueNums>0){ finalMessageQueuemq=tpInfo.selectOneMessageQueue(); if(notBestBroker!=null){ mq.setBrokerName(notBestBroker); mq.setQueueId(tpInfo.getSendWhichQueue().getAndIncrement()%writeQueueNums); } returnmq; }else{ latencyFaultTolerance.remove(notBestBroker); } }catch(Exceptione){ log.error("Erroroccurredwhenselectingmessagequeue",e); } returntpInfo.selectOneMessageQueue(); } //不开启异常规避,则随机自增选择Queue returntpInfo.selectOneMessageQueue(lastBrokerName); } 四、RocketMQ 的三种 CommunicationMode 源码清单-12 publicenumCommunicationMode{ SYNC, ASYNC, ONEWAY, } 以上三种模式指的都是消息从发送方到达 broker 的阶段,不包含 broker 将消息投递给订阅方的过程。三种模式的发送方式的差异: 单向模式:ONEWAY。消息发送方只管发送,并不关心 broker 处理的结果如何。这种模式下,由于处理流程少,发送耗时非常小,吞吐量大,但不能保证消息可靠不丢,常用于流量巨大但不重要的消息场景,例如心跳发送等。 异步模式:ASYNC。消息发送方发送消息到 broker 后,无需等待 broker 处理,拿到的是 null 的返回值,而由一个异步的线程来做消息处理,处理完成后以回调的形式告诉发送方发送结果。异步处理时如有异常,返回发送方失败结果之前,会经过内部重试(默认 3 次,发送方不感知)。这种模式下,发送方等待时长较小,吞吐量较大,消息可靠,用于流量大但重要的消息场景。 同步模式:SYNC。消息发送方需等待 broker 处理完成并明确返回成功或失败,在消息发送方拿到消息发送失败的结果之前,也会经历过内部重试(默认 3 次,发送方不感知)这种模式下,发送方会阻塞等待消息处理结果,等待时长较长,消息可靠,用于流量不大但重要的消息场景。需要强调的是,事务消息的一阶段半事务消息的处理是同步模式。 在 sendKernelImpl 方法中也可以看到具体的实现差异。ONEWAY 模式最为简单,不做任何处理。负责发送的 sendMessage 方法参数中,相比同步模式,异步模式多了回调方法、包含 topic 发送路由元信息的 topicPublishInfo、包含发送 broker 信息的 instance、包含发送队列信息的 producer、重试次数。另外,异步模式下,会对有压缩的消息先做 copy。 源码清单-13 switch(communicationMode){ caseASYNC: MessagetmpMessage=msg; booleanmessageCloned=false; if(msgBodyCompressed){ //Ifmsgbodywascompressed,msgbodyshouldberesetusingprevBody. //Clonenewmessageusingcommpressedmessagebodyandrecoveroriginmassage. //Fixbug:https://github.com/apache/rocketmq-externals/issues/66 tmpMessage=MessageAccessor.cloneMessage(msg); messageCloned=true; msg.setBody(prevBody); } if(topicWithNamespace){ if(!messageCloned){ tmpMessage=MessageAccessor.cloneMessage(msg); messageCloned=true; } msg.setTopic(NamespaceUtil.withoutNamespace(msg.getTopic(),this.defaultMQProducer.getNamespace())); } longcostTimeAsync=System.currentTimeMillis()-beginStartTime; if(timeout<costTimeAsync){ thrownewRemotingTooMuchRequestException("sendKernelImplcalltimeout"); } sendResult=this.mQClientFactory.getMQClientAPIImpl().sendMessage( brokerAddr, mq.getBrokerName(), tmpMessage, requestHeader, timeout-costTimeAsync, communicationMode, sendCallback, topicPublishInfo, this.mQClientFactory, this.defaultMQProducer.getRetryTimesWhenSendAsyncFailed(), context, this); break; caseONEWAY: caseSYNC: longcostTimeSync=System.currentTimeMillis()-beginStartTime; if(timeout<costTimeSync){ thrownewRemotingTooMuchRequestException("sendKernelImplcalltimeout"); } sendResult=this.mQClientFactory.getMQClientAPIImpl().sendMessage( brokerAddr, mq.getBrokerName(), msg, requestHeader, timeout-costTimeSync, communicationMode, context, this); break; default: assertfalse; break; } 官方文档中有这样一张图,十分清晰的描述了异步通信的详细过程: Ⅱ 阶段发送 源码清单-3 体现了本地事务的执行,localTransactionState 将本地事务执行结果与事务消息二阶段的发送关联起来。 值得注意的是,如果一阶段的发送结果是 SLAVENOTAVAILABLE,即便 broker 不可用时,也会将 localTransactionState 置为 Rollback,此时将不会执行本地事务。之后由 endTransaction 方法负责二阶段提交,见源码清单-4。具体到 endTransaction 的实现: 源码清单-14 publicvoidendTransaction( finalSendResultsendResult, finalLocalTransactionStatelocalTransactionState, finalThrowablelocalException)throwsRemotingException,MQBrokerException,InterruptedException,UnknownHostException{ finalMessageIdid; if(sendResult.getOffsetMsgId()!=null){ id=MessageDecoder.decodeMessageId(sendResult.getOffsetMsgId()); }else{ id=MessageDecoder.decodeMessageId(sendResult.getMsgId()); } StringtransactionId=sendResult.getTransactionId(); finalStringbrokerAddr=this.mQClientFactory.findBrokerAddressInPublish(sendResult.getMessageQueue().getBrokerName()); EndTransactionRequestHeaderrequestHeader=newEndTransactionRequestHeader(); requestHeader.setTransactionId(transactionId); requestHeader.setCommitLogOffset(id.getOffset()); switch(localTransactionState){ caseCOMMIT_MESSAGE: requestHeader.setCommitOrRollback(MessageSysFlag.TRANSACTION_COMMIT_TYPE); break; caseROLLBACK_MESSAGE: requestHeader.setCommitOrRollback(MessageSysFlag.TRANSACTION_ROLLBACK_TYPE); break; caseUNKNOW: requestHeader.setCommitOrRollback(MessageSysFlag.TRANSACTION_NOT_TYPE); break; default: break; } requestHeader.setProducerGroup(this.defaultMQProducer.getProducerGroup()); requestHeader.setTranStateTableOffset(sendResult.getQueueOffset()); requestHeader.setMsgId(sendResult.getMsgId()); Stringremark=localException!=null?("executeLocalTransactionBranchexception:"+localException.toString()):null; //采用oneway的方式发送二阶段消息 this.mQClientFactory.getMQClientAPIImpl().endTransactionOneway(brokerAddr,requestHeader,remark, this.defaultMQProducer.getSendMsgTimeout()); } 在二阶段发送时,之所以用 oneway 的方式发送,个人理解这正是因为事务消息有一个特殊的可靠机制——回查。消息回查 当 Broker 经过了一个特定的时间,发现依然没有得到事务消息的二阶段是否要提交或者回滚的确切信息,Broker 不知道 Producer 发生了什么情况(可能 producer 挂了,也可能 producer 发了 commit 但网络抖动丢了,也可能……于是主动发起回查。 事务消息的回查机制,更多的是在 broker 端的体现。RocketMQ 的 broker 以 Half 消息、Op 消息、真实消息三个不同的 topic 来将不同发送阶段的事务消息进行了隔离,使得 Consumer 只能看到最终确认 commit 需要投递出去的消息。其中详细的实现逻辑在本文中暂不多赘述,后续可另开一篇专门来从 Broker 视角来解读。 回到 Producer 的视角,当收到了 Broker 的回查请求,Producer 将根据消息检查本地事务状态,根据结果决定提交或回滚,这就要求 Producer 必须指定回查实现,以备不时之需。当然,正常情况下,并不推荐主动发送 UNKNOW 状态,这个状态毫无疑问会给 broker 带来额外回查开销,只在出现不可预知的异常情况时才启动回查机制,是一种比较合理的选择。 另外,4.7.1 版本的事务回查并非无限回查,而是最多回查 15 次: 源码清单-15 /** *Themaximumnumberoftimesthemessagewaschecked,ifexceedthisvalue,thismessagewillbediscarded. */ @ImportantField privateinttransactionCheckMax=15;附录 官方给出 Producer 的默认参数如下(其中超时时长的参数,在前文中也已经提到,debug 的结果是默认 3000ms,并非 10000ms):

优秀的个人博客,低调大师

我从 17w star 的 Vuejs 中学到了什么?

本文来源公众号:「HcySunYang」 框架设计远没有大家想的那么简单,并不是说只把功能开发完成,能用就算完事儿了,这里面还是有很多学问的。比如说,我们的框架应该给用户提供哪些构建产物?产物的模块格式如何?当用户没有以预期的方式使用框架时是否应该打印合适的警告信息从而提升更好的开发体验,让用户快速定位问题?开发版本的构建和生产版本的构建有何区别?热跟新(HMR:Hot Module Replacement)需要框架层面的支持才行,我们是否也应该考虑?再有就是当你的框架提供了多个功能,如果用户只需要其中几个功能,那么用户是否可以选择关闭其他功能从而减少资源的打包体积?所有以上这些问题我们都会在本节内容进行讨论。 本节内容需要大家对常用的模块打包工具有一定的使用经验,尤其是 rollup.js 以及 webpack。如果你只用过或了解过其中一个也没关系,因为它们很多概念其实是类似的。如果你没有使用任何模块打包工具那么需要你自行去了解一下,至少有了初步认识之后再来看本节内容会更好一些。 提升用户的开发体验 衡量一个框架是否足够优秀的指标之一就是看它的开发体验如何,我们拿 Vue3 举个例子: createApp(App).mount('#not-exist') 当我们创建一个 Vue 应用并试图将其挂载到一个不存在的 DOM 节点时就会得到一个警告信息: warn 从这条信息中我们得知挂载失败了,并说明了失败的原因:Vue 根据我们提供的选择器无法找到相应的 DOM 元素(返回 null),正式因为这条信息的存在使得我们能够清晰且快速的了解并定位问题,可以试想一下如果 Vue 内部不做任何处理,那么很可能得到的是一个 JS 层面的错误信息,例如:Uncaught TypeError: Cannot read property 'xxx' of null,但是根据此信息我们很难知道问题出在哪里。 所以在框架设计和开发的过程中,提供友好的警告信息是至关重要的,如果这一点做得不好那么很可能经常收到用户的抱怨。始终提供友好的警告信息不仅能够快速帮助用户定位问题,节省用户的时间,还能够为框架收获良好的口碑,让用户认为你是非常专业的。 在 Vue 的源码中,你经常能够看到 warn() 函数的调用,例如上面图片中的信息就是由这句 warn() 函数调用打印的: warn( `Failedtomountapp:mounttargetselector"${container}"returnednull.` ) 对于 warn() 函数来说,由于它需要尽可能的提供有用的信息,因此它需要收集当前发生错误的组件的组件栈信息,所以如果你去看源码你会发现有些复杂,但其实最终就是调用了 console.warn() 函数。 对于开发体验来说,除了提供必要的警告信息,还有很多其他方面可以作为切入口,可以进一步提升用户的开发体验。例如在 Vue3 中当我们在控制台打印一个 Ref 数据时: constcount=ref(0) console.log(count) 打开控制台查看输出,如下图所示: 没有任何处理的输出 可以发现非常的不直观,当然我们可以直接打印 count.value ,这样就只会输出 0,但是有没有办法在打印 count 的时候让输出的信息更有好呢?当然可以,浏览允许我们编写自定义的 formatter,从而自定义输出的形式。在 Vue 的源码中你可以搜索到名为 initCustomFormatter 的函数,这个函数就是用来在开发环境下初始化自定义 formatter 的,以 chrome 为例我们可以打开 devtool 的设置,然后勾选 Console -> Enable custom formatters: 然后刷新浏览器后查看控制台,会发现输出的内容变得非常直观: 控制框架代码的体积 框架的大小也是衡量框架的标准之一,在实现同样功能的情况下当然是用越少的代码越好,这样体积就会越小,最后浏览器加载资源的时间也就越少。这时我们不禁会想,提供越完善的警告信息就意味着我们要编写更多的代码,这不是与控制代码体积相驳吗?没错,所以我们要想办法解决这个问题。 如果我们去看 Vue 的源码会发现,每一个 warn() 函数的调用都会配合 __DEV__ 常量的检查,例如: if(__DEV__&&!res){ warn( `Failedtomountapp:mounttargetselector"${container}"returnednull.` ) } 可以看到,打印警告信息的前提是:__DEV__ 这个常量一定要为真,这里的 __DEV__ 常量就是达到目的的关键。 Vue 使用的是 rollup.js 对项目进行构建的,这里的 __DEV__ 常量实际上是通过 rollup 的配置来预定义的,其功能类似于 webpack 中的 DefinePlugin 插件。 Vue 在输出资源的时候,会输出两个版本的资源,其中一个资源用于开发环境,如 vue.global.js ;另一个与其对应的用于生产环境,如:vue.global.prod.js ,通过文件名称我们也能够区分。 当 Vue 构建用于开发环境的资源时,会把 __DEV__ 常量设置为 true,这时上面那段输出警告信息的代码就等价于: if(true&&!res){ warn( `Failedtomountapp:mounttargetselector"${container}"returnednull.` ) } 可以看到这里的 __DEV__ 被替换成了字面量 true ,所以这段代码在开发环境是肯定存在的。 当 Vue 构建用于生产环境的资源时,会把 __DEV__ 常量设置为 false,这时上面那段输出警告信息的代码就等价于: if(false&&!res){ warn( `Failedtomountapp:mounttargetselector"${container}"returnednull.` ) } 可以看到 __DEV__ 常量被替换为字面量 false ,这时我们发现这段分支代码永远都不会执行,因为判断条件始终为假,这段永远不会执行的代码被称为 Dead Code,它不会出现在最终的产物中,在构建资源的时候就会被移除,因此在 vue.global.prod.js 中是不会存在这段代码的。 这样我们就做到了在开发环境为用户提供友好的警告信息的同时,还不会增加生产环境代码的体积。 框架要做到良好的 Tree-Shaking 上文中我们提到通过构建工具设置预定义的常量 __DEV__ ,就能够做到在生产环境使得框架不包含打印警告信息的代码,从而使得框架自身的代码量变少。但是从用户的角度来看,这么做仍然不够,还是拿 Vue 来举个例子,我们知道 Vue 提供了内置的组件例如 <Transition> ,如果我们的项目中根本就没有使用到该组件,那么 <Transition> 组件的代码需要包含在我们项目最终的构建资源中吗?答案是当然不需要,那如何做到这一点呢?这就不得不提到本节的主角 Tree-Shaking。 那什么是 Tree-Shaking 呢?在前端领域这个概念因 rollup 而普及,简单的说所谓 **Tree-Shaking **指的就是消除哪些永远不会执行的代码,也就是排除 dead-code,现在无论是 rollup 还是 webpack 都支持 Tree-Shaking。 想要实现 Tree-Shaking 必须满足一个条件,即模块必须是 ES Module,因为 Tree-Shaking 依赖 ESM 的静态结构。我们使用 rollup 通过一个简单的例子看看 Tree-Shaking 如何工作,我们 demo 的目录结构如下: ├──demo │└──package.json │└──input.js │└──utils.js 首先安装 rollup: yarnaddrollup-D#或者npminstallrollup-D 下面是 input.js 和 utils.js 文件的内容: //input.js import{foo}from'./utils.js' foo() //utils.js exportfunctionfoo(obj){ obj&&obj.foo } exportfunctionbar(obj){ obj&&obj.bar } 代码很简单,我们在 utils.js 文件中定义并导出了两个函数,分别是 foo 和 bar,然后在 input.js 中导入了 foo 函数并执行,注意我们并没有导入 bar 函数。 接着我们执行如下命令使用 rollup 构建: npxrollupinput.js-fesm-obundle.js 这句命令的意思是以 input.js 文件问入口,输出 ESM 模块,输出的文件名叫做 bundle.js 。命令执行成功后,我们打开 bundle.js 来查看一下它的内容: //bundle.js functionfoo(obj){ obj&&obj.foo } foo(); 可以看到,其中并不包含 bar 函数,这说明 Tree-Shaking 起了作用,由于我们并没有使用 bar 函数,因此它作为 dead-code 被删除了。但是如果我们仔细观察会发现,foo 函数的执行也没啥意义呀,就是读取了对象的值,所以它执行还是不执行也没有本质的区别呀,所以即使把这段代码删了,也对我们的应用没啥影响,那为什么 rollup 不把这段代码也作为 dead-code 移除呢? 这就涉及到 Tree-Shaking 中的第二个关键点,即副作用。如果一个函数调用会产生副作用,那么就不能将其移除。什么是副作用?简单地说副作用的意思是当调用函数的时候,会对外部产生影响,例如修改了全局变量。这时你可能会说,上面的代码明显是读取对象的值怎么会产生副作用呢?其实是有可能的,想想一下如果 obj 对象是一个通过 Proxy 创建的代理对象那么当我们读取对象属性时就会触发 Getter ,在 Getter 中是可能产生副作用的,例如我们在 Getter 中修改了某个全局变量。而到底会不会产生副作用,这个只有代码真正运行的时候才能知道, JS 本身是动态语言,想要静态的分析哪些代码是 dead-code 是一件很有难度的事儿,上面只是举了一个简单的例子。 正因为静态分析 JS 代码很困难,所以诸如 rollup 等这类工具都会给我提供一个机制,让我们有能力明确的告诉 rollup :”放心吧,这段代码不会产生副作用,你可以放心移除它“,那具体怎么做呢?如下代码所示,我们修改 input.js 文件: import{foo}from'./utils' /*#__PURE__*/foo() 注意这段注释代码 /*#__PURE_*_/,该注释的作用就是用来告诉 rollup 对于 foo() 函数的调用不会产生副作用,你可以放心的对其进行 Tree-Shaking,此时再次执行构建命令并查看 bundle.js 文件你会发现它的内容是空的,这说明 Tree-Shaking 生效了。 基于这个案例大家应该明白的是,在编写框架的时候我们需要合理的使用 /*#__PURE_*_/ 注释,如果你去搜索 Vue 的源码会发现它大量的使用了该注释,例如下面这句: exportconstisHTMLTag=/*#__PURE__*/makeMap(HTML_TAGS) 也许你会觉得这会不会对编写代码带来很大的心智负担?其实不会,这是因为通常产生副作用的代码都是模块内函数的顶级调用,什么是顶级调用呢?如下代码所示: foo()//顶级调用 functionbar(){ foo()//函数内调用 } 可以看到对于顶级调用来说是可能产生副作用的,但对于函数内调用来说只要函数 bar 没有被调用,那么 foo 函数的调用当然不会产生副作用。因此你会发现在 Vue 的源码中,基本都是在一些顶级调用的函数上使用 /*#__PURE__*/ 注释的。当然该注释不仅仅作用与函数,它可以使用在任何语句上,这个注释也不是只有 rollup 才能识别,webpack 以及压缩工具如 terser 都能识别它。 框架应该输出怎样的构建产物 上文中我们提到 Vue 会为开发环境和生产环境输出不同的包,例如 vue.global.js 用于开发环境,它包含了必要的警告信息,而 vue.global.prod.js 用于生产环境,不包含警告信息。实际上 Vue 的构建产物除了有环境上的区分之外,还会根据使用场景的不同而输出其他形式的产物,这一节我们将讨论这些产物的用途以及在构建阶段如何输出这些产物。 不同类型的产物一定是有对应的需求背景的,因此我们从需求讲起。首先我们希望用户可以直接在 html 页面中使用 <script> 标签引入框架并使用: <body> <scriptsrc="/path/to/vue.js"></script> <script> const{createApp}=Vue //... </script> </body> 为了能够实现这个需求,我们就需要输出一种叫做 IIFE 格式的资源,IIFE 的全称是 Immediately Invoked Function Expression ,即”立即调用的函数表达式“,可以很容易的用 JS 来表达: (function(){ //... }()) 如上代码所示,这就是一个立即执行的函数表达式。实际上 vue.globale.js 文件就是 IIFE 形式的资源,大家可以看一下它的代码结构: varVue=(function(exports){ //... exports.createApp=createApp; //... returnexports }({})) 这样当我们使用 <script> 标签直接引入 vue.global.js 文件后,那么全局变量 Vue 就是可用的了。 在 rollup 中我们可以通过配置 format: 'iife' 来实现输出这种形式的资源: //rollup.config.js constconfig={ input:'input.js', output:{ file:'output.js', format:'iife'//指定模块形式 } } exportdefaultconfig 不过随着技术的发展和浏览器的支持,现在主流浏览器对原生 ESM 模块的支持都不错,所以用户除了能够使用 <script> 标签引用 IIFE 格式的资源外,还可以直接引如 ESM 格式的资源,例如 Vue3 会输出 vue.esm-browser.js 文件,用户可以直接用 <script> 标签引入: <scripttype="module"src="/path/to/vue.esm-browser.js"></script> 为了输出 ESM 格式的资源就需要我们配置 rollup 的输出格式为:format: 'esm'。 你可能已经注意到了,为什么 vue.esm-browser.js 文件中会有 -browser 字样,其实对于 ESM 格式的资源来说,Vue 还会输出一个 vue.esm-bundler.js 文件,其中 -browser 变成了 -bundler。为什么这么做呢?我们知道无论是 rollup 还是 webpack 在寻找资源时,如果 package.json 中存在 module 字段,那么会优先使用 module 字段指向的资源来代替 main 字段所指向的资源。我们可以打开 Vue 源码中的 packages/vue/package.json 文件看一下: { "main":"index.js", "module":"dist/vue.runtime.esm-bundler.js", } 其中 module 字段指向的是 vue.runtime.esm-bundler.js 文件,意思就是说如果你的项目是使用 webpack 构建的,那你使用的 Vue 资源就是 vue.runtime.esm-bundler.js ,也就是说带有 -bundler 字样的 ESM 资源是给 rollup 或 webpack 等打包工具使用的,而带有 -browser 字样的 ESM 资源是直接给 <script type="module"> 去使用的。 那他们之间的区别是什么呢?那这就不得不提到上文中的 __DEV__ 常量,当构建用于 <script> 标签的 ESM 资源时,如果是用于开发环境,那么 __DEV__ 会设置为 true;如果是用于生产环境,那么 __DEV__ 常量会被设置为 false ,从而被 Tree-Shaking 移除。但是当我们构建提供给打包工具的 ESM 格式的资源时,我们不能直接把 __DEV__ 设置为 true 或 false,而是使用 (process.env.NODE_ENV !== 'production') 替换掉 __DEV__ 常量。例如下面的源码: if(__DEV__){ warn(`useCssModule()isnotsupportedintheglobalbuild.`) } 在带有 -bundler 字样的资源中会变成: if((process.env.NODE_ENV!=='production')){ warn(`useCssModule()isnotsupportedintheglobalbuild.`) } 这样用户侧的 webpack 配置可以自己决定构建资源的目标环境,但是最终的效果其实是一样的,这段代码也只会出现在开发环境。 用户除了可以直接使用 <script> 标签引入资源,我们还希望用户可以在 Node.js 中通过 require 语句引用资源,例如: constVue=require('vue') 为什么会有这种需求呢?答案是服务端渲染,当服务端渲染时 Vue 的代码是运行在 Node.js 环境的,而非浏览器环境,在 Node.js 环境下资源的模块格式应该是 CommonJS ,简称 cjs。为了能够输出 cjs 模块的资源,我们可以修改 rollup 的配置:format: 'cjs' 来实现: //rollup.config.js constconfig={ input:'input.js', output:{ file:'output.js', format:'cjs'//指定模块形式 } } exportdefaultconfig 特性开关 在设计框架时,框架会提供诸多特性(或功能)给用户,例如我们提供 A、B、C 三个特性给用户,同时呢我们还提供了 a、b、c 三个对应的特性开关,用户可以通过设置 a、b、c 为 true 和 false 来代表开启和关闭,那么将会带来很多收益: 对于用户关闭的特性,我们可以利用 Tree-Shaking 机制让其不包含在最终的资源中。 该机制为框架设计带来了灵活性,可以通过特性开关任意为框架添加新的特性而不用担心用不到这些特性的用户侧资源体积变大,同时当框架升级时,我们也可以通过特性开关来支持遗留的 API,这样新的用户可以选择不适用遗留的 API,从而做到用户侧资源最小化。 那怎么实现特性开关呢?其实很简单,原理和上文提到的 __DEV__ 常量一样,本质是利用 rollup 的预定义常量插件来实现,那一段 Vue3 的 rollup 配置来看: { __FEATURE_OPTIONS_API__:isBundlerESMBuild?`__VUE_OPTIONS_API__`:true, } 其中 __FEATURE_OPTIONS_API__ 类似于 __DEV__,我们可以在 Vue3 的源码中搜索,可以找到很多类似如下代码这样的判断分支: //supportfor2.xoptions if(__FEATURE_OPTIONS_API__){ currentInstance=instance pauseTracking() applyOptions(instance,Component) resetTracking() currentInstance=null } 当 Vue 构建资源时,如果构建的资源是用于给打包工具使用的话(即带有 -bundler 字样的资源),那么上面代码在资源中会变成: //supportfor2.xoptions if(__VUE_OPTIONS_API__){//这一这里 currentInstance=instance pauseTracking() applyOptions(instance,Component) resetTracking() currentInstance=null } 其中 __VUE_OPTIONS_API__ 就是一个特性开关,用户侧就可以通过设置 __VUE_OPTIONS_API__ 来控制是否包含这段代码。通常用户可以使用 webpack.DefinePlugin 插件实现: //webpack.DefinePlugin插件配置 newwebpack.DefinePlugin({ __VUE_OPTIONS_API__:JSON.stringify(true)//开启特性 }) 最后再来详细解释一下 __VUE_OPTIONS_API__ 开关是干嘛用的,在 Vue2 中我们编写的组件叫做组件选项 API: exportdefault{ data(){},//data选项 computed:{},//computed选项 //其他选项... } 但是在 Vue3 中,更推荐使用 Composition API 来编写代码,例如: exportdefault{ setup(){ constcount=ref(0) constdoubleCount=computed(()=>count.value*2)//相当于Vue2中的computed选项 } } 但是为了兼容 Vue2,在 Vue3 中仍然可以使用选项 API 的方式编写代码,但是对于明确知道自己不会使用选项 API 的用户来说,它们就可以选择使用 __VUE_OPTIONS_API__ 开关来关闭该特性,这样在打包的时候 Vue 的这部分代码就不会包含在最终的资源中,从而减小资源体积。 错误处理 错误处理是开发框架的过程中非常重要的环节,框架的错误处理做的好坏能够直接决定用户应用程序的健壮性,同时还决定了用户开发应用时处理错误的心智负担。 为了让大家对错误处理的重要性有更加直观的感受,我们从一个小例子说起。假设我们开发了一个工具模块,代码如下: //utils.js exportdefault{ foo(fn){ fn&&fn() } } 该模块导出一个对象,其中 foo 属性是一个函数,接收一个回调函数作为参数,调用 foo 函数时会执行回调函数,在用户侧使用时: importutilsfrom'utils.js' utils.foo(()=>{ //... }) 大家思考一下如果用户提供的回调函数在执行的时候出错了怎么办?此时有两个办法,其一是让用户自行处理,这需要用户自己去 try...catch: importutilsfrom'utils.js' utils.foo(()=>{ try{ //... }catch(e){ //... } }) 但是这对用户来说是增加了负担,试想一下如果 utils.js 不是仅仅提供了一个 foo 函数,而是提供了几十上百个类似的函数,那么用户在使用的时候就需要逐一添加错误处理程序。 第二种办法是我们代替用户统一处理错误,如下代码所示: //utils.js exportdefault{ foo(fn){ try{ fn&&fn() }catch(e){/*...*/} }, bar(fn){ try{ fn&&fn() }catch(e){/*...*/} }, } 这中办法其实就是我们代替用户编写错误处理程序,实际上我们可以进一步封装错误处理程序为一个函数,假设叫它 callWithErrorHandling: //utils.js exportdefault{ foo(fn){ callWithErrorHandling(fn) }, bar(fn){ callWithErrorHandling(fn) }, } functioncallWithErrorHandling(fn){ try{ fn&&fn() }catch(e){ console.log(e) } } 可以看到代码变得简洁多了,但简洁不是目的,这么做真正的好处是,我们有机会为用户提供统一的错误处理接口,如下代码所示: //utils.js lethandleError=null exportdefault{ foo(fn){ callWithErrorHandling(fn) }, //用户可以调用该函数注册统一的错误处理函数 resigterErrorHandler(fn){ handleError=fn } } functioncallWithErrorHandling(fn){ try{ fn&&fn() }catch(e){ //捕获到的错误传递给用户的错误处理程序 handleError(e) } } 我们提供了 resigterErrorHandler 函数,用户可以使用它注册错误处理程序,然后在 callWithErrorHandling 函数内部捕获到错误时,把错误对象传递给用户注册的错误处理程序。 这样在用户侧的代码就会非常简洁且健壮: importutilsfrom'utils.js' //注册错误处理程序 utils.resigterErrorHandler((e)=>{ console.log(e) }) utils.foo(()=>{/*...*/}) utils.bar(()=>{/*...*/}) 这时错误处理的能力完全由用户控制,用户既可以选择忽略错误,也可以调用上报程序将错误上报到监控系统。 实际上这就是 Vue 错误处理的原理,你可以在源码中搜索到 callWithErrorHandling 函数,另外在 Vue 中我们也可以注册统一的错误处理函数: importAppfrom'App.vue' constapp=createApp(App) app.config.errorHandler=()=>{ //错误处理程序 } 良好的 Typescript 类型支持 Typescript 是微软开源的编程语言,简称 TS,它是 JS 的超集能够为 JS 提供类型支持。现在越来越多的人和团队在他们的项目中使用 TS 语言,使用 TS 的好处很多,如代码即文档、编辑器的自动提示、一定程度上能够避免低级 bug、让代码的可维护性更强等等。因此对 TS 类型支持的是否完善也成为评价一个框架的重要指标。 那如何衡量一个框架对 TS 类型支持的好坏呢?这里有一个常见的误区,很多同学以为只要是使用 TS 编写就是对 TS 类型支持的友好,其实使用 TS 编写框架和框架对 TS 类型支持的友好是两件关系不大的事儿。考虑到有的同学可能没有接触过 TS,所以这里不会做深入讨论,我们只举一个简单的例子,如下是使用 TS 编写的函数: functionfoo(val:any){ returnval } 这个函数很简单,它接受一个参数 val 并且参数可以是任意类型(any),该函数直接将参数作为返回值,这说明返回值的类型是由参数决定的,参数如果是 number 类型那么返回值也是 number 类型,然后我们可以尝试使用一下这个函数,如下图所示: 类型支持不友好 在调用 foo 函数时我们传递了一个字符串类型的参数 'str',按照之前的分析,我们得到的结果 res 的类型应该也是字符串类型,然而当我们把鼠标 hover 到 res 常量上时可以看到其类型是 any,这并不是我们想要的结果,为了达到理想状态我们只需要对 foo 函数做简单的修改即可: functionfoo<Textendsany>(val:T):T{ returnval } 大家不需要理解这段代码,我们直接来看一下现在的表现: 类型友好 可以看到 res 的类型是字符字面量 'str' 而不是 any 了,这说明我们的代码生效了。 通过这个简单的例子我们认识到,使用 TS 编写代码与对 TS 类型支持友好是两件事,在编写大型框架时想要做到完美的 TS 类型支持是一件很不容易的事情,大家可以查看 Vue 源码中的 runtime-core/src/apiDefineComponent.ts 文件,整个文件里真正会在浏览器运行的代码其实只有 3 行,但是当你打开这个文件的时候你会发现它整整有接近 200 行的代码,其实这些代码都是在做类型支持方面的事情,由此可见框架想要做到完善的类型支持是需要付出相当大的努力的。 除了要花大力气做类型推导,从而做到更好的类型支持外,还要考虑对 TSX 的支持,我们会单独一篇来详细讨论。 以上,欢迎分享、关注。 ❤️ 谢谢支持 1.喜欢的话别忘了分享、点赞、在看三连哦~。 2.长按下方图片,关注「前端从进阶到入院」,获取我分类整理的原创精选面试热点文章,Vue、React、TS、手写题等应有尽有,我进入字节、滴滴、百度的小伙伴们说文章对他们帮助很大,希望也能帮助到你。 本文分享自微信公众号 - 前端从进阶到入院(code_with_love)。 如有侵权,请联系 support@oschina.cn 删除。 本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

资源下载

更多资源
Mario

Mario

马里奥是站在游戏界顶峰的超人气多面角色。马里奥靠吃蘑菇成长,特征是大鼻子、头戴帽子、身穿背带裤,还留着胡子。与他的双胞胎兄弟路易基一起,长年担任任天堂的招牌角色。

腾讯云软件源

腾讯云软件源

为解决软件依赖安装时官方源访问速度慢的问题,腾讯云为一些软件搭建了缓存服务。您可以通过使用腾讯云软件源站来提升依赖包的安装速度。为了方便用户自由搭建服务架构,目前腾讯云软件源站支持公网访问和内网访问。

Nacos

Nacos

Nacos /nɑ:kəʊs/ 是 Dynamic Naming and Configuration Service 的首字母简称,一个易于构建 AI Agent 应用的动态服务发现、配置管理和AI智能体管理平台。Nacos 致力于帮助您发现、配置和管理微服务及AI智能体应用。Nacos 提供了一组简单易用的特性集,帮助您快速实现动态服务发现、服务配置、服务元数据、流量管理。Nacos 帮助您更敏捷和容易地构建、交付和管理微服务平台。

WebStorm

WebStorm

WebStorm 是jetbrains公司旗下一款JavaScript 开发工具。目前已经被广大中国JS开发者誉为“Web前端开发神器”、“最强大的HTML5编辑器”、“最智能的JavaScript IDE”等。与IntelliJ IDEA同源,继承了IntelliJ IDEA强大的JS部分的功能。

用户登录
用户注册