<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://me.bitgong.cn/feed.xml" rel="self" type="application/atom+xml" /><link href="https://me.bitgong.cn/" rel="alternate" type="text/html" /><updated>2026-08-28T10:41:52+00:00</updated><id>https://me.bitgong.cn/feed.xml</id><title type="html">bitgong - 游戏技术架构师</title><subtitle>拥有10+年游戏开发经验的技术分享平台，专注于Cocos Creator游戏开发。 深度分享开发工具使用、踩坑经验总结、技术原理解析， 致力于帮助游戏开发者提升技能，携手共同成长。</subtitle><author><name>bitgong</name></author><entry><title type="html">我把 nvm、pyenv、jenv 全换成了 mise</title><link href="https://me.bitgong.cn/posts/%E6%88%91%E6%8A%8A-nvm-pyenv-jenv-%E5%85%A8%E6%8D%A2%E6%88%90%E4%BA%86mise/" rel="alternate" type="text/html" title="我把 nvm、pyenv、jenv 全换成了 mise" /><published>2026-08-28T02:00:00+00:00</published><updated>2026-08-28T02:00:00+00:00</updated><id>https://me.bitgong.cn/posts/%E6%88%91%E6%8A%8A-nvm-pyenv-jenv-%E5%85%A8%E6%8D%A2%E6%88%90%E4%BA%86mise</id><content type="html" xml:base="https://me.bitgong.cn/posts/%E6%88%91%E6%8A%8A-nvm-pyenv-jenv-%E5%85%A8%E6%8D%A2%E6%88%90%E4%BA%86mise/"><![CDATA[<h1 id="我把-nvmpyenvjenv-全换成了-mise">我把 nvm、pyenv、jenv 全换成了 mise</h1>

<p>我平时工作接触的东西比较杂。虽然是游戏开发，但用到的工具其实不少：Android 打包要用 Java，平时写脚本会用 Python 或者 JavaScript，还要维护项目的 GM 后台前端。</p>

<p>关键是不同项目依赖的版本还可能不一样。从 GitHub 上 clone 下来的项目，因为环境版本不对出现 bug 的情况，我也遇到过不少次。</p>

<p>以前我分别用 <code class="language-plaintext highlighter-rouge">nvm</code>、<code class="language-plaintext highlighter-rouge">pyenv</code> 和 <code class="language-plaintext highlighter-rouge">jenv</code> 管理这些版本，单独使用也没觉得有什么大问题。</p>

<p>直到最近整理了一下系统环境，这个问题就暴露出来了：<code class="language-plaintext highlighter-rouge">.zshrc</code> 里 Node 一套初始化，Python 一套初始化，Java 又是一套初始化。三个工具各管各的，命令不一样，配置位置不一样，出了问题还得分别排查。</p>

<p>说实话，不是 <code class="language-plaintext highlighter-rouge">nvm</code>、<code class="language-plaintext highlighter-rouge">pyenv</code>、<code class="language-plaintext highlighter-rouge">jenv</code> 不好用。单独拿出来，它们都能把自己的事情做好。真正让我难受的是：<strong>为了管理三个运行时，我也得同时管理三套版本管理工具。</strong></p>

<p>所以我就搜了一下有没有开源工具能把这些事情统一起来，结果找到了 <a href="https://mise.jdx.dev/">mise 官网</a> 和 <a href="https://github.com/jdx/mise">GitHub 仓库</a>。截至 2026 年 8 月，GitHub 上已经有 33.1K Star。</p>

<p>这次我干脆全部换成了 mise。用了几天之后，我的感觉是：这种工具就应该统一啊，GitHub 真是一个宝库。</p>

<h2 id="最烦的不是装版本而是版本不一致">最烦的不是装版本，而是版本不一致</h2>

<p>自己一个人开发时，版本不一致还不算特别明显。机器上默认是什么版本，项目通常就先用什么版本，遇到问题再切一下。</p>

<p>团队开发就不一样了。</p>

<p>同一份代码，在我电脑上能跑，到同事电脑上突然报一个莫名其妙的错。查半天业务逻辑，最后发现一个人用 Node 20，另一个人用 Node 22；或者 Python 的小版本不同，某个依赖装出来的结果不一样；再或者 Android 项目要求 JDK 17，某台机器的 <code class="language-plaintext highlighter-rouge">JAVA_HOME</code> 还指向 JDK 11。</p>

<p>这种 bug 最恶心的地方，是它看起来像代码问题，实际上是环境问题。</p>

<p>大家在群里说一句“这个项目要用 Node 20”没什么用，README 里写一遍也不保险。真正可靠的方式，是把工具版本和代码放在一起，让项目自己声明：<strong>我到底需要什么环境。</strong></p>

<p>这正是 mise 最打动我的地方。</p>

<h2 id="mise-是什么">mise 是什么</h2>

<p>可以先把 mise 理解成一个开发工具版本管理器。</p>

<p>以前是：</p>

<ul>
  <li>Node.js 交给 <code class="language-plaintext highlighter-rouge">nvm</code></li>
  <li>Python 交给 <code class="language-plaintext highlighter-rouge">pyenv</code></li>
  <li>Java 交给 <code class="language-plaintext highlighter-rouge">jenv</code></li>
</ul>

<p>现在是：</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Node.js ─┐
Python  ─┼─&gt; mise
Java    ─┘
</code></pre></div></div>

<p>而且它不只管这三种。Go、Ruby、Rust、Terraform、各种 npm、pipx、GitHub Release 里的 CLI 工具，也都可以放进同一套配置里管理。</p>

<p>当然，我目前真正高频使用的还是 Node.js、Python，所以这篇只聊我实际用到的部分，不拿“支持上千种工具”这种数字凑功能表。</p>

<h2 id="安装之后zshrc-终于清净了">安装之后，<code class="language-plaintext highlighter-rouge">.zshrc</code> 终于清净了</h2>

<p>我在 Mac 上通过 Homebrew 安装：</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>brew <span class="nb">install </span>mise
</code></pre></div></div>

<p>然后在 <code class="language-plaintext highlighter-rouge">.zshrc</code> 里激活：</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">eval</span> <span class="s2">"</span><span class="si">$(</span>mise activate zsh<span class="si">)</span><span class="s2">"</span>
</code></pre></div></div>

<p>重新打开终端后，可以跑一下：</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>mise doctor
</code></pre></div></div>

<p>以前为了三个版本管理工具，要分别初始化、分别处理 <code class="language-plaintext highlighter-rouge">PATH</code> 和环境变量。现在核心配置只剩这一份。</p>

<p>这一点听起来不算什么大功能，但迁移过一次电脑就知道有多舒服。以后换机器，我不用再回忆 <code class="language-plaintext highlighter-rouge">nvm</code> 怎么装、<code class="language-plaintext highlighter-rouge">pyenv</code> 还缺哪些编译依赖、<code class="language-plaintext highlighter-rouge">jenv</code> 的初始化顺序应该放在哪。先装 mise，再交给它统一处理就行。</p>

<h2 id="全局默认版本一条命令就够了">全局默认版本，一条命令就够了</h2>

<p>平时总得有一套默认环境，可以直接这样配置：</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>mise use <span class="nt">--global</span> node@x.x.x python@x.x.x java@x.x.x
</code></pre></div></div>

<p>这条命令会安装对应工具 (版本号换成自己用的)，并把它们写进 mise 的全局配置。之后在没有项目级配置的目录里，就使用这套默认版本。</p>

<p>检查当前环境也很直接：</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>node <span class="nt">--version</span>
python <span class="nt">--version</span>
java <span class="nt">--version</span>
<span class="nb">echo</span> <span class="nv">$JAVA_HOME</span>
</code></pre></div></div>

<h2 id="最舒服的是项目级版本">最舒服的是项目级版本</h2>

<p>全局版本只是基础，mise 真正解决团队问题的，是项目里的 <code class="language-plaintext highlighter-rouge">mise.toml</code>。</p>

<p>比如进入一个项目后执行：</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>mise use node@22.14.0 python@3.12.9 java@22.0.0
</code></pre></div></div>

<p>mise 会在当前目录创建或更新 <code class="language-plaintext highlighter-rouge">mise.toml</code>：</p>

<div class="language-toml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nn">[tools]</span>
<span class="py">node</span> <span class="p">=</span> <span class="s">"22.14.0"</span>
<span class="py">python</span> <span class="p">=</span> <span class="s">"3.12.9"</span>
<span class="py">java</span> <span class="p">=</span> <span class="s">"22.0.0"</span>
</code></pre></div></div>

<p>把这个文件提交到 Git，工具版本就不再是某个人电脑里的隐形配置，而是项目的一部分。</p>

<p>团队成员拉下代码后，执行：</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>mise <span class="nb">install</span>
</code></pre></div></div>

<p>需要的版本就会按项目配置安装好。进入这个目录，终端自动使用项目指定的 Node.js、Python 和 Java；离开目录，又恢复到全局默认版本或者另一个项目的版本。</p>

<p>整个切换过程不需要我手动执行 <code class="language-plaintext highlighter-rouge">nvm use</code>、<code class="language-plaintext highlighter-rouge">pyenv local</code> 或者再改一次 <code class="language-plaintext highlighter-rouge">JAVA_HOME</code>。这几天在 Mac 上用下来，目录切换基本没什么存在感，很丝滑。</p>

<p>我觉得这才是版本管理工具该有的状态：<strong>平时感觉不到它，只有环境不对时才意识到它替你挡掉了一个坑。</strong></p>

<p>以前 Python 需要一个 <code class="language-plaintext highlighter-rouge">.python-version</code>，Node.js 需要一个 <code class="language-plaintext highlighter-rouge">.nvmrc</code>，现在一份 <code class="language-plaintext highlighter-rouge">mise.toml</code> 就能搞定。</p>

<h2 id="做个临时小东西也不用污染全局环境">做个临时小东西，也不用污染全局环境</h2>

<p>我平时经常会临时写个脚本、验证一个想法。这种小东西可能今天用 Node，明天用 Python，过几天自己都忘了当时是什么版本。</p>

<p>以前通常懒得专门配置，直接拿全局版本跑。几个月之后再打开，突然跑不起来了，还得猜当时到底用了什么环境。</p>

<p>现在即使只是个小目录，也可以顺手执行：</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>mise use node@22
</code></pre></div></div>

<p>多一个很小的 <code class="language-plaintext highlighter-rouge">mise.toml</code>，换来的是这个目录以后随时打开都知道该用什么版本。这个习惯对“大项目”当然有用，但我觉得它对那些生命周期不确定的小工具更有价值。</p>

<p>如果只是临时跑一次，连配置文件都不想创建，也可以这样：</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>mise <span class="nb">exec </span>python@3.12 <span class="nt">--</span> python script.py
</code></pre></div></div>

<p>它会在指定的 Python 环境里执行脚本，不需要先把全局版本切来切去。</p>

<h2 id="它不只是版本管理器">它不只是版本管理器</h2>

<p>我一开始只是想用 mise 替代 <code class="language-plaintext highlighter-rouge">nvm</code>、<code class="language-plaintext highlighter-rouge">pyenv</code> 和 <code class="language-plaintext highlighter-rouge">jenv</code>，但它实际上还可以管理项目环境变量和任务。</p>

<p>比如：</p>

<div class="language-toml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nn">[tools]</span>
<span class="py">node</span> <span class="p">=</span> <span class="s">"22.14.0"</span>

<span class="nn">[env]</span>
<span class="py">NODE_ENV</span> <span class="p">=</span> <span class="s">"development"</span>

<span class="nn">[tasks]</span>
<span class="py">dev</span> <span class="p">=</span> <span class="s">"npm run dev"</span>
<span class="py">build</span> <span class="p">=</span> <span class="s">"npm run build"</span>
</code></pre></div></div>

<p>之后可以统一执行：</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>mise run dev
mise run build
</code></pre></div></div>

<p>这样工具版本、环境变量和常用命令都放在一个文件里。新同事接手项目时，不需要先看半天文档，再手动拼出一套本地环境。</p>

<p>不过我现在没有急着把所有脚本都迁进去。版本管理已经解决了我最主要的问题，任务和环境变量准备后面按项目需要慢慢用。工具功能多不代表必须一次全上，先解决真实痛点更重要。</p>

<h2 id="我最喜欢的三个地方">我最喜欢的三个地方</h2>

<h3 id="第一一个入口">第一，一个入口</h3>

<p>脑子里不用再记三套命令。想装版本、切版本、看当前版本，全部先找 mise。</p>

<p>工具一多，统一入口带来的不是少打几条命令，而是少了一整套上下文切换。</p>

<h3 id="第二项目环境可以提交到-git">第二，项目环境可以提交到 Git</h3>

<p>口头约定和 README 都会过期，配置文件才是能执行的约定。</p>

<p>只要团队统一安装 mise，拉代码后就能得到相同的运行时版本。它不可能消灭所有“我这里能跑”的问题，但至少能先把 Node、Python、Java 版本不同这一大类问题排除掉。</p>

<h3 id="第三迁移机器轻松很多">第三，迁移机器轻松很多</h3>

<p>以前重装系统，需要分别安装 <code class="language-plaintext highlighter-rouge">nvm</code>、<code class="language-plaintext highlighter-rouge">pyenv</code>、<code class="language-plaintext highlighter-rouge">jenv</code> 和 JDK，再用不同的方式恢复各个项目需要的版本。现在只需要先装 mise，剩下的工具配置都能从全局配置和各项目的 <code class="language-plaintext highlighter-rouge">mise.toml</code> 恢复。</p>

<p>对于我这种今天写 TypeScript、明天跑 Python、后天又要打 Android 包的人，这种统一感比单项功能多强大更重要。</p>

<h2 id="翻了翻其他人的评测优缺点还挺一致">翻了翻其他人的评测，优缺点还挺一致</h2>

<p>为了确认是不是刚换工具带来的新鲜感，我又看了几篇其他开发者的长期使用评测。</p>

<p>不过评价也不全是夸。在一篇 [Reddit 讨论]: https://www.reddit.com/r/webdev/comments/1hiripn/anybody_have_experience_with_mise_considering_it/ 里，比较集中的问题有两个：一是 VS Code 等从桌面启动的 IDE 有时拿不到终端里的 PATH，需要单独处理；二是冷门工具依赖不同 backend，质量和稳定性不一定像 Node.js、Python、Java 这些内置核心工具一样稳定。也有人对部分版本升级时的行为变化有意见。</p>

<p>所以我的判断没变：<strong>如果主要管理 Node.js、Python、Java 这类常用工具，mise 已经很好用了；如果项目依赖比较冷门的 SDK 或插件，先在自己的环境里完整跑一遍安装、切换和 CI，再决定要不要全团队迁移。</strong></p>

<h2 id="目前遇到和需要注意的地方">目前遇到和需要注意的地方</h2>

<p>mise 也不是装完就能让所有软件自动理解你的环境，有几个边界最好提前知道。</p>

<p>第一，<strong>终端切换成功，不代表已经打开的 IDE 也会立刻切换。</strong></p>

<p>Java 版本变化后，依赖 <code class="language-plaintext highlighter-rouge">JAVA_HOME</code> 的 IDE 可能需要重启。Android Studio 自己也有 Gradle JDK 配置，Gradle Daemon 还可能继续使用之前启动时的 Java。终端里 <code class="language-plaintext highlighter-rouge">java --version</code> 正确，只能证明当前终端正确，打包前最好再确认一下 Gradle 实际用了哪个 JDK。</p>

<p>第二，<strong>只使用 shims 时，部分环境变量能力不完整。</strong></p>

<p>例如 Java 的 <code class="language-plaintext highlighter-rouge">JAVA_HOME</code> 需要 <code class="language-plaintext highlighter-rouge">mise activate</code>、<code class="language-plaintext highlighter-rouge">mise exec</code> 或 <code class="language-plaintext highlighter-rouge">mise run</code> 提供完整环境，不能只看到 <code class="language-plaintext highlighter-rouge">java</code> 命令能运行就以为全部配置好了。交互式终端我更推荐按官方方式完整激活；CI 和脚本里则可以显式使用 <code class="language-plaintext highlighter-rouge">mise exec</code> 或 <code class="language-plaintext highlighter-rouge">mise run</code>。</p>

<p>第三，<strong>陌生项目的配置别无脑信任。（项目配置是可以执行行为的, 这里可能会有恶意代码注入 或者 信息泄露风险，需谨慎）</strong></p>

<p>别人提交的 <code class="language-plaintext highlighter-rouge">mise.toml</code> 如果包含环境指令、Hook 或任务，可能执行项目定义的命令，存在恶意代码执行或信息泄露风险。mise 遇到未信任的配置时可能会提示 <code class="language-plaintext highlighter-rouge">mise trust</code>，但也不要把这行提示当成唯一的安全检查：当前普通模式下，<code class="language-plaintext highlighter-rouge">mise install</code>、<code class="language-plaintext highlighter-rouge">mise run</code>、<code class="language-plaintext highlighter-rouge">mise exec</code> 等显式执行项目行为的命令会自动信任当前配置。网上随便拉下来的仓库，先看看配置写了什么，再执行这些命令。</p>

<p>第四，<strong>旧版本文件的兼容要自己确认。</strong></p>

<p>mise 可以识别 <code class="language-plaintext highlighter-rouge">.nvmrc</code>、<code class="language-plaintext highlighter-rouge">.python-version</code>、<code class="language-plaintext highlighter-rouge">.java-version</code> 这类已有文件，但目前这些“惯用版本文件”默认并不是全部自动启用。如果正在迁移老项目，不要看到文件还在就默认 mise 一定读取了，先用 <code class="language-plaintext highlighter-rouge">mise config ls</code> 和实际版本确认一下。我的选择是新项目统一使用 <code class="language-plaintext highlighter-rouge">mise.toml</code>，规则更直观。</p>

<h2 id="windows-支持但我没测过">Windows 支持，但我没测过</h2>

<p>mise 官方支持 Windows 和 PowerShell，Node.js、Python 等核心后端也提供 Windows 支持。官方文档还专门处理了 Windows 下 Python 可执行文件和 <code class="language-plaintext highlighter-rouge">pip</code> 包装脚本的问题。</p>

<p>因为我手上没有 Windows 设备，没有真实跑过安装、自动切换、PowerShell 激活和 IDE 集成，不清楚是否和 Mac 上体验一样丝滑。</p>

<p>Mac 上这几天的体验我可以负责：安装简单，切目录很顺，Node.js、Python、Java 放在一起管理之后，整个开发环境清爽了很多。Windows 到底有没有路径、权限或者终端兼容方面的小坑，还是得实际用过才知道。</p>

<h2 id="最后附一份常用命令">最后附一份常用命令</h2>

<p>下面这些基本覆盖了我日常管理 Node.js、Python 和 Java 的场景。命令里的 <code class="language-plaintext highlighter-rouge">x.x.x</code> 换成实际版本号。</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 安装 mise</span>
brew <span class="nb">install </span>mise

<span class="c"># 安装后把这一行加入 .zshrc，重新打开终端生效</span>
<span class="nb">eval</span> <span class="s2">"</span><span class="si">$(</span>mise activate zsh<span class="si">)</span><span class="s2">"</span>

<span class="c"># 查看当前生效版本</span>
mise current
mise current node
mise <span class="nb">ls</span>

<span class="c"># 查看远端可安装版本</span>
mise ls-remote java
mise ls-remote node
mise ls-remote python

<span class="c"># 设置全局默认版本</span>
mise use <span class="nt">--global</span> python@x.x.x
mise use <span class="nt">--global</span> node@x.x.x
mise use <span class="nt">--global</span> java@x.x.x

<span class="c"># 为当前项目设置版本，同时写入当前目录的 mise.toml</span>
mise use python@x.x.x
mise use node@x.x.x
mise use java@x.x.x

<span class="c"># 安装当前项目 mise.toml 中声明的全部工具</span>
mise <span class="nb">install</span>

<span class="c"># 临时切换当前终端的版本</span>
mise shell java@x.x.x

<span class="c"># 查看当前实际执行的命令，以及对应工具的安装目录</span>
mise which node
mise where node

<span class="c"># 检查可更新项并升级</span>
mise outdated
mise upgrade

<span class="c"># 仅安装指定版本，不写入 mise.toml，也不切换当前版本</span>
mise <span class="nb">install </span>python@x.x.x
mise <span class="nb">install </span>node@x.x.x

<span class="c"># 卸载指定的已安装版本</span>
mise uninstall node@x.x.x

<span class="c"># 环境诊断</span>
mise doctor
</code></pre></div></div>

<p>这里有几个容易搞混的地方：</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">mise use</code> 是“安装并使用”，还会把版本写入全局配置或当前项目的 <code class="language-plaintext highlighter-rouge">mise.toml</code>；</li>
  <li><code class="language-plaintext highlighter-rouge">mise install</code> 只是安装。后面带版本号时不会自动写配置，也不会让这个版本立即生效；不带参数时则安装当前配置声明的全部工具；</li>
  <li><code class="language-plaintext highlighter-rouge">mise shell</code> 只影响当前终端，而且要求当前 shell 已经执行过 <code class="language-plaintext highlighter-rouge">mise activate</code>；关闭终端后这次切换就没了；</li>
  <li><code class="language-plaintext highlighter-rouge">mise upgrade</code> 默认只在配置允许的版本范围内升级。如果 <code class="language-plaintext highlighter-rouge">mise.toml</code> 写死了完整版本号，想升级并改写配置，需要先看 <code class="language-plaintext highlighter-rouge">mise outdated --bump</code>，再执行 <code class="language-plaintext highlighter-rouge">mise upgrade --bump</code>；</li>
  <li><code class="language-plaintext highlighter-rouge">mise uninstall</code> 只删除本机安装的工具版本，不会删除 <code class="language-plaintext highlighter-rouge">mise.toml</code> 里的配置。如果项目仍然引用这个版本，下次执行 <code class="language-plaintext highlighter-rouge">mise install</code> 还会重新装回来。</li>
</ul>

<h2 id="总结">总结</h2>

<p>如果你只写一种语言，而且现有的 <code class="language-plaintext highlighter-rouge">nvm</code> 或 <code class="language-plaintext highlighter-rouge">pyenv</code> 已经用得很舒服，完全没必要为了追新工具专门折腾。mise 最大的价值，不是它把某一种语言管理得比所有专用工具都强，而是它能用同一种方式管理很多工具。</p>

<p>如果你和我一样：</p>

<ul>
  <li>项目类型比较杂，Node.js、Python、Java 来回切；</li>
  <li>经常因为团队成员环境版本不同遇到奇怪问题；</li>
  <li><code class="language-plaintext highlighter-rouge">.zshrc</code> 里塞了好几套版本管理工具的初始化；</li>
  <li>希望项目自己声明工具版本，换电脑后也能快速恢复；</li>
</ul>

<p>那 mise 很值得试一下。</p>

<p>我目前的评价很简单：<strong>它没有发明版本管理这件事，只是终于把散落在各个语言里的版本管理，收进了同一个工具。</strong></p>

<p>用了几天之后，我已经不太想回到 <code class="language-plaintext highlighter-rouge">nvm</code>、<code class="language-plaintext highlighter-rouge">pyenv</code>、<code class="language-plaintext highlighter-rouge">jenv</code> 各管一摊的状态了。</p>]]></content><author><name>bitgong</name></author><category term="博客文章" /><category term="mise" /><category term="开发环境" /><category term="版本管理" /><category term="Node.js" /><category term="Python" /><category term="Java" /><summary type="html"><![CDATA[Node.js、Python、Java 分别交给 nvm、pyenv、jenv 管理，带来的不只是三套命令，还有混乱的 shell 初始化和难以复现的项目环境。我把它们统一迁移到 mise，用一份 mise.toml 锁定团队工具版本，并分享项目切换、临时执行、IDE 集成、安全信任和旧版本兼容中的真实体验与边界。]]></summary></entry><entry><title type="html">被老板一句话逼出来的（纯野路子）硬编码文本多语言方案：怎么在基本不影响日常开发的前提下，把翻译的路提前铺好</title><link href="https://me.bitgong.cn/posts/%E6%B8%B8%E6%88%8F%E5%A4%9A%E8%AF%AD%E8%A8%80%E9%80%82%E9%85%8D-%E4%B8%8D%E5%BD%B1%E5%93%8D%E6%97%A5%E5%B8%B8%E5%BC%80%E5%8F%91%E7%9A%84%E9%87%8E%E8%B7%AF%E5%AD%90/" rel="alternate" type="text/html" title="被老板一句话逼出来的（纯野路子）硬编码文本多语言方案：怎么在基本不影响日常开发的前提下，把翻译的路提前铺好" /><published>2026-08-21T02:00:00+00:00</published><updated>2026-08-21T02:00:00+00:00</updated><id>https://me.bitgong.cn/posts/%E6%B8%B8%E6%88%8F%E5%A4%9A%E8%AF%AD%E8%A8%80%E9%80%82%E9%85%8D-%E4%B8%8D%E5%BD%B1%E5%93%8D%E6%97%A5%E5%B8%B8%E5%BC%80%E5%8F%91%E7%9A%84%E9%87%8E%E8%B7%AF%E5%AD%90</id><content type="html" xml:base="https://me.bitgong.cn/posts/%E6%B8%B8%E6%88%8F%E5%A4%9A%E8%AF%AD%E8%A8%80%E9%80%82%E9%85%8D-%E4%B8%8D%E5%BD%B1%E5%93%8D%E6%97%A5%E5%B8%B8%E5%BC%80%E5%8F%91%E7%9A%84%E9%87%8E%E8%B7%AF%E5%AD%90/"><![CDATA[<h1 id="被老板一句话逼出来的纯野路子硬编码文本多语言方案怎么在基本不影响日常开发的前提下把翻译的路提前铺好">被老板一句话逼出来的（纯野路子）硬编码文本多语言方案：怎么在基本不影响日常开发的前提下，把翻译的路提前铺好</h1>

<p>之前做的一款游戏，上线运营都两年了，然后老板一句话：这游戏要发海外。</p>

<p>当时直接懵逼了。</p>

<p>那个项目里不止有超级多的硬编码中文，还有各种拼接出来的中文字符串，外加超级多带文字的图。界面设计的时候压根没考虑过多语言，文本框就那么点大，英文塞进去直接溢出。这种情况没有任何取巧的办法，只能把所有文本全部翻一遍、逐个硬找。幸亏现在有 AI，让 AI 先快速过一遍把文本全扒出来，再人工一条条处理，不然纯靠人肉翻代码，工期根本不敢想。</p>

<p>所以，以后设计游戏架构，得把多语言提前考虑进去。不一定用得上，但是万一哪天老板又来一句”发海外”，至少不用再经历一遍这种痛苦。</p>

<p>所以就有了下面这套东西。先说明，这不是什么业界标准方案，<strong>纯野路子</strong>——我自己琢磨出来的一种”既方便后期转多语言，又基本不影响日常开发”的适配方式。</p>

<p>先说明一下范围：下面主要讲的是<strong>代码里硬编码文本</strong>怎么处理，带文字的图和界面布局这两个坑不好用同一套办法解决，放到最后简单聊聊。</p>

<p>有一个前提：<strong>硬编码文本还是尽量写中文</strong>。毕竟是中国人，显示界面中的文本写个中文看起来多顺眼。如果为了多语言，日常开发全写 <code class="language-plaintext highlighter-rouge">I18n.get("ui_btn_confirm")</code> 这种 key，对于我这种英语不太好的人来说，看不懂啊，难受啊。所以我的目标是：平时写代码就当没有多语言这回事，只在需要的时候能够快速支持其他语言。</p>

<p>下面就讲讲具体是怎么做</p>

<h2 id="核心思路两层-key">核心思路：两层 key</h2>

<p>多语言最常见的做法是”文案表”：代码里只写 key，运行时拿 key 查表得到当前语言的文案。问题在于 key 是要人来起的，一句”确定”你得先想好它叫 <code class="language-plaintext highlighter-rouge">common_ok</code> 还是 <code class="language-plaintext highlighter-rouge">dialog_confirm</code>，再去表里登记，写代码的节奏就被打断了。</p>

<p>我的做法是把 key 分成两层。</p>

<p>分之前先说一个中文特有的坑：<strong>同一个中文词，在不同语境下翻译可能完全不同</strong>。比如”装备”，作名词是背包里的装备，英文是 equipment；作动词是把装备穿上，英文是 equip。如果两处都直接写中文，就只能共用一条译文，必有一处译错。这种词必须起独立的 key、分开翻译——这就是常量层存在的核心原因之一。</p>

<p><strong>第一层：常量 key</strong>。短词、会被多处复用的文案 或者 相同的中文但是需要不同的翻译文本的，在 <code class="language-plaintext highlighter-rouge">TextKey.ts</code> 里集中定义：</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cm">/**
 * 简中文案表 属性名即key 值即中文 新增文案在这里加一行
 * 本表同时就是简中语言的文案表 所以简中不需要加载任何资源
 */</span>
<span class="k">export</span> <span class="kd">const</span> <span class="nx">ZH_TEXTS</span> <span class="o">=</span> <span class="p">{</span>
    <span class="cm">/********** 通用 **********/</span>
    <span class="na">common_ok</span><span class="p">:</span> <span class="dl">"</span><span class="s2">确定</span><span class="dl">"</span><span class="p">,</span>
    <span class="na">common_cancel</span><span class="p">:</span> <span class="dl">"</span><span class="s2">取消</span><span class="dl">"</span><span class="p">,</span>
    <span class="na">common_close</span><span class="p">:</span> <span class="dl">"</span><span class="s2">关闭</span><span class="dl">"</span>
<span class="p">}</span> <span class="k">as</span> <span class="kd">const</span><span class="p">;</span>
</code></pre></div></div>

<p>用的时候 <code class="language-plaintext highlighter-rouge">I18n.get(TK.common_ok)</code>，有 IDE 补全，key 拼错了 TS 编译直接报错。</p>

<p><strong>第二层：中文原文本身就是 key</strong>。只出现一次的长句，根本不起 key，直接把中文传进去：</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">ui</span><span class="p">.</span><span class="nx">WindowManager</span><span class="p">.</span><span class="nx">showWindow</span><span class="p">(</span><span class="nx">Alert</span><span class="p">,</span> <span class="p">{</span>
    <span class="na">title</span><span class="p">:</span> <span class="nx">I18n</span><span class="p">.</span><span class="kd">get</span><span class="p">(</span><span class="dl">"</span><span class="s2">更新提示</span><span class="dl">"</span><span class="p">),</span>
    <span class="na">content</span><span class="p">:</span> <span class="nx">I18n</span><span class="p">.</span><span class="kd">get</span><span class="p">(</span><span class="dl">"</span><span class="s2">检测到{mb}MB的更新资源, 是否更新？</span><span class="dl">"</span><span class="p">,</span> <span class="p">{</span> <span class="na">mb</span><span class="p">:</span> <span class="nx">result</span><span class="p">.</span><span class="nx">size</span> <span class="o">/</span> <span class="mi">1024</span> <span class="p">}),</span>
    <span class="na">okTitle</span><span class="p">:</span> <span class="nx">I18n</span><span class="p">.</span><span class="kd">get</span><span class="p">(</span><span class="dl">"</span><span class="s2">更新</span><span class="dl">"</span><span class="p">),</span>
    <span class="na">cancelTitle</span><span class="p">:</span> <span class="nx">I18n</span><span class="p">.</span><span class="kd">get</span><span class="p">(</span><span class="dl">"</span><span class="s2">退出游戏</span><span class="dl">"</span><span class="p">)</span>
<span class="p">});</span>
</code></pre></div></div>

<p>写起来跟硬编码几乎没有区别，只是外面套了一层 <code class="language-plaintext highlighter-rouge">I18n.get()</code>。查表的时候 key 就是这句中文本身，查不到就直接返回 key——反正 key 就是原文。</p>

<p>这一层，图的就是省事：写这句话的时候完全不用想”这个 key 该叫什么、该去哪个文件加一行”，写完中文往外面套一层函数就完事，不用跳出去改配置文件。</p>

<p>两层的分工很简单：<strong>短词、复用的走常量；一次性的长句直接用中文</strong>。什么时候该把中文提成常量，交给检查工具去提醒（后面讲），开发的时候不用纠结。</p>

<h2 id="简中零成本不加载任何资源">简中零成本：不加载任何资源</h2>

<p>这套设计里我最满意的一点是：<strong>简体中文不需要任何额外资源</strong>。</p>

<p><code class="language-plaintext highlighter-rouge">ZH_TEXTS</code> 这张表本身就是简中的文案表，初始化时直接灌进内存；中文原文 key 更省事，查不到表就返回 key 本身，天然就是正确文案：</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">public</span> <span class="k">static</span> <span class="nx">init</span><span class="p">(</span><span class="nx">lang</span><span class="p">:</span> <span class="nx">Language</span><span class="p">):</span> <span class="k">void</span> <span class="p">{</span>
    <span class="k">this</span><span class="p">.</span><span class="nx">_lang</span> <span class="o">=</span> <span class="nx">lang</span><span class="p">;</span>
    <span class="k">this</span><span class="p">.</span><span class="nx">_texts</span><span class="p">.</span><span class="nx">clear</span><span class="p">();</span>
    <span class="k">if</span> <span class="p">(</span><span class="nx">lang</span> <span class="o">===</span> <span class="nx">Language</span><span class="p">.</span><span class="nx">ZH</span><span class="p">)</span> <span class="p">{</span>
        <span class="c1">// 简中直接拿 ZH_TEXTS 当文案表 不读任何资源</span>
        <span class="kd">const</span> <span class="na">texts</span><span class="p">:</span> <span class="nb">Readonly</span><span class="o">&lt;</span><span class="nb">Record</span><span class="o">&lt;</span><span class="kr">string</span><span class="p">,</span> <span class="kr">string</span><span class="o">&gt;&gt;</span> <span class="o">=</span> <span class="nx">ZH_TEXTS</span><span class="p">;</span>
        <span class="k">for</span> <span class="p">(</span><span class="kd">const</span> <span class="nx">key</span> <span class="k">of</span> <span class="nb">Object</span><span class="p">.</span><span class="nx">keys</span><span class="p">(</span><span class="nx">texts</span><span class="p">))</span> <span class="p">{</span>
            <span class="k">this</span><span class="p">.</span><span class="nx">_texts</span><span class="p">.</span><span class="kd">set</span><span class="p">(</span><span class="nx">key</span><span class="p">,</span> <span class="nx">texts</span><span class="p">[</span><span class="nx">key</span><span class="p">]);</span>
        <span class="p">}</span>
    <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
        <span class="k">this</span><span class="p">.</span><span class="nx">loadTexts</span><span class="p">(</span><span class="nx">lang</span><span class="p">);</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>只有非中文语言，才去加载对应语言的文案表 json（按语言分 bundle，<code class="language-plaintext highlighter-rouge">language-en/hardcode.json</code> 这种）：</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">language</span> <span class="o">=</span> <span class="nx">LanguageHelper</span><span class="p">.</span><span class="nx">language</span><span class="p">;</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">language</span> <span class="o">!==</span> <span class="nx">Language</span><span class="p">.</span><span class="nx">ZH</span><span class="p">)</span> <span class="p">{</span>
    <span class="c1">// 加载对应语言的文件, 方便之后使用</span>
<span class="p">}</span>
</code></pre></div></div>

<p>也就是说，只要这个项目不出海，多语言这一整套东西对包体、对加载流程、对运行时性能、对平常开发的便捷程度的影响几乎都是零。这就是我说的”不影响日常开发”。</p>

<h2 id="禁止拼接老项目里最大的坑">禁止拼接：老项目里最大的坑</h2>

<p>回到开头那个老项目，最要命的其实不是硬编码多——硬编码再多吃苦也能翻完。真正折磨人的是<strong>拼接字符串</strong>：</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// 反面教材</span>
<span class="nx">text</span> <span class="o">=</span> <span class="dl">"</span><span class="s2">恭喜你获得了</span><span class="dl">"</span> <span class="o">+</span> <span class="nx">count</span> <span class="o">+</span> <span class="dl">"</span><span class="s2">个</span><span class="dl">"</span> <span class="o">+</span> <span class="nx">itemName</span><span class="p">;</span>
</code></pre></div></div>

<p>这种句子没法整体进文案表，因为变量部分的取值无法穷举。更要命的是语言之间的语序不一样，中文”获得了3个金币”，英文是 “Obtained 3 gold coins”，”个”这种量词在英文里根本不存在，按词拆开翻译再拼接，拼出来全是病句。</p>

<p>所以这套方案里，动态内容统一用 <code class="language-plaintext highlighter-rouge">{name}</code> 占位符：</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">I18n</span><span class="p">.</span><span class="kd">get</span><span class="p">(</span><span class="dl">"</span><span class="s2">检测到{mb}MB的更新资源, 是否更新？</span><span class="dl">"</span><span class="p">,</span> <span class="p">{</span> <span class="na">mb</span><span class="p">:</span> <span class="nx">size</span> <span class="p">});</span>
</code></pre></div></div>

<p>整句进文案表，翻译可以看到完整语境，变量位置随便挪。</p>

<p>那有人非要写拼接怎么办？靠自觉是不行的，得靠工具拦。</p>

<h2 id="工具链交给-ai-写就行">工具链：交给 AI 写就行</h2>

<p>光靠自觉是不够的，得有工具拦一下。我写了几个脚本放在 <code class="language-plaintext highlighter-rouge">tools/i18n/</code> 下，核心是用 TypeScript Compiler API 做 AST 扫描——比正则靠谱，能准确分清参数到底是字符串字面量、<code class="language-plaintext highlighter-rouge">TK.xxx</code> 常量引用，还是拼出来的模板串。</p>

<p>脚本主要干两件事：</p>

<ul>
  <li><strong>导出待翻译文案</strong>：把 <code class="language-plaintext highlighter-rouge">ZH_TEXTS</code> 的全部条目、加上代码里所有 <code class="language-plaintext highlighter-rouge">I18n.get("中文…")</code> 的字面量，合并成一份 json，丢给翻译或者 AI 去翻，翻完回填到对应语言的文案表；</li>
  <li><strong>一致性检查</strong>：拦掉模板串/<code class="language-plaintext highlighter-rouge">+</code> 拼接的写法，拦掉”某句中文已经在 <code class="language-plaintext highlighter-rouge">ZH_TEXTS</code> 里定义过又被人以原文重复写了一遍”的情况，顺带提醒哪些中文重复出现太多次该提成常量了。</li>
</ul>

<p>导出的 json 具体怎么用，再说细一点：
翻译同学或者策划不一定爱看 json，这块可以再写个工具把这份数据转成 Excel、CSV 或者任何他们习惯用的格式——具体导出成什么样、字段怎么排，看自己团队的需求，这种转换脚本 AI 写起来也很顺手。</p>

<p>这几个脚本具体怎么写我就不展开了——这种活儿现在直接丢给 AI 写就行，把规则讲清楚，AST 怎么解析、判断逻辑怎么写，AI 都能给你搞出来。写完之后接进 pre-commit，提交前跑一下增量检查，写错了当场打回；CI 上再跑一次全量扫描保底就好了。</p>

<h2 id="这套方案没解决什么">这套方案没解决什么</h2>

<p>说实话，它只覆盖了”代码里的文本”，老项目里另外两个坑它管不了：</p>

<ol>
  <li><strong>带文字的图</strong>。我的做法是：把这类图按语言分开放进不同的 bundle，路径和文件名保持一致。非中文语言加载图的时候，先去对应语言的 bundle 里找同路径同名的图，找到了就用，找不到就 fallback 回中文那张。这样哪个语言的图还没配齐，也不会直接崩掉，慢慢补就行；</li>
  <li><strong>界面布局</strong>。这个真没有野路子，英文平均比中文长 30% 以上，文本框不预留空间必溢出，只能在设计阶段就把这事考虑进去——文本框给够、用自适应布局，别用固定宽度写死。</li>
</ol>

<p>另外这套扫描只覆盖代码，FGUI 编辑器里直接填的静态文本也在射程之外，项目里的约定是：界面文本统一走代码赋值，FGUI 里只放占位——这条约定本身也是为了方便扫描。</p>

<h2 id="最后总结">最后总结</h2>

<p>整套东西说白了就是想解决一个问题：<strong>开发的时候压根不想惦记多语言这回事，能不能等真要出海了再一次性搞定</strong>。</p>

<ol>
  <li><strong>两层 key</strong>：短词复用、语境不同翻译不同的走常量表；一次性长句直接写中文，中文本身就是 key，不用跳出去加配置；</li>
  <li><strong>简中零成本</strong>：不加载任何资源，不出海就当这套东西不存在；</li>
  <li><strong>禁止拼接</strong>：动态内容一律 <code class="language-plaintext highlighter-rouge">{name}</code> 占位符，让翻译看到整句；</li>
  <li><strong>工具兜底</strong>：检测和导出脚本让 AI 写，pre-commit/CI 拦一下违规写法就行。</li>
</ol>

<p>日常开发的体验几乎没有变化——写中文、正常提交，唯一的额外动作是偶尔多套一层 <code class="language-plaintext highlighter-rouge">I18n.get()</code>。但如果哪天真要出海，跑一次导出脚本，所有需要翻译的文案就整整齐齐躺在一份 json 里了。</p>

<p>这只是我自己琢磨出来的一个野路子，肯定不是什么标准答案，带文字的图和界面布局那两块也还有很多细节没讲透，有更好的方式欢迎交流讨论。核心思路就一条：<strong>日常开发的时候，脑子里只装中文就够了</strong>。</p>]]></content><author><name>bitgong</name></author><category term="博客文章" /><category term="多语言适配" /><category term="i18n" /><category term="游戏开发" /><category term="Cocos Creator" /><category term="国际化" /><summary type="html"><![CDATA[运营两年的游戏突然被一句'发海外'，硬编码中文全部返工。于是我琢磨出一套野路子：两层key——短词走常量表、一次性长句用中文原文当key；简中零成本；禁止拼接统一占位符；AST扫描工具兜底。不影响日常开发，提前铺好翻译的路，不出海代价近乎为零。]]></summary></entry><entry><title type="html">值得每个程序员认真读不止一遍的《代码整洁之道》究竟说了些什么？</title><link href="https://me.bitgong.cn/posts/%E4%BB%A3%E7%A0%81%E6%95%B4%E6%B4%81%E4%B9%8B%E9%81%93-%E6%A0%B8%E5%BF%83%E8%A6%81%E7%82%B9/" rel="alternate" type="text/html" title="值得每个程序员认真读不止一遍的《代码整洁之道》究竟说了些什么？" /><published>2026-08-20T02:00:00+00:00</published><updated>2026-08-20T02:00:00+00:00</updated><id>https://me.bitgong.cn/posts/%E4%BB%A3%E7%A0%81%E6%95%B4%E6%B4%81%E4%B9%8B%E9%81%93-%E6%A0%B8%E5%BF%83%E8%A6%81%E7%82%B9</id><content type="html" xml:base="https://me.bitgong.cn/posts/%E4%BB%A3%E7%A0%81%E6%95%B4%E6%B4%81%E4%B9%8B%E9%81%93-%E6%A0%B8%E5%BF%83%E8%A6%81%E7%82%B9/"><![CDATA[<h1 id="值得每个程序员认真读不止一遍的代码整洁之道究竟说了些什么">值得每个程序员认真读不止一遍的《代码整洁之道》究竟说了些什么？</h1>

<p>这本书我真的是读过5遍以上了，每次看还都能有一些收获，这本书究竟讲了些什么呢？他不讲数据结构，不讲语法，只说了代码怎么写好维护。下面听老宫展开讲讲吧</p>

<h2 id="童子军军规">童子军军规</h2>

<p>全书我记得最牢的一句话，反而不是哪条具体规则，是那句童子军军规：离开营地时，让它比你来的时候更干净。</p>

<p>以前改一个 bug，顺手改完就走，绝不多碰一行——生怕改多了引入新问题，也没那个心思。现在心态变了：改到哪块代码，只要顺路，看到一个命名很烂就顺手改了，看到一段重复逻辑就顺手提出来。不是刻意去做”重构任务”，就是路过的时候擦一下灰。这个习惯坚持了几个月，明显感觉到自己维护的几个模块比以前顺眼多了（不过这里又违背了开闭原则）</p>

<h2 id="命名很重要一定要可搜索">命名很重要（一定要可搜索）</h2>

<p>书里讲命名那一章，说实话道理都不新鲜，但我读的时候是确实有点羞耻。<code class="language-plaintext highlighter-rouge">data</code>、<code class="language-plaintext highlighter-rouge">info</code>、<code class="language-plaintext highlighter-rouge">temp</code>、<code class="language-plaintext highlighter-rouge">flag</code>，这些词我写代码的时候用得飞起，图省事，反正自己知道是什么意思。</p>

<p>问题是三个月之后我自己都不知道那个 <code class="language-plaintext highlighter-rouge">flag</code> 到底控制的是什么。后来养成一个习惯：写变量名的时候，多花两秒问自己一句”如果这行代码脱离上下文单独拿出来，我还能看懂吗”。答不上来就换个名字。布尔变量尤其如此，<code class="language-plaintext highlighter-rouge">isXxx</code>、<code class="language-plaintext highlighter-rouge">hasXxx</code> 这种前缀，看着啰嗦，但比 <code class="language-plaintext highlighter-rouge">flag</code> 强一百倍。</p>

<p>魔法数字也是，之前写超时时间直接写 <code class="language-plaintext highlighter-rouge">3600</code>，谁看了都得反应半天是不是一小时。现在起码会给它起个名字，哪怕只是个局部常量。</p>

<h2 id="函数要短-只做一件事本来就短">函数要短 (只做一件事,本来就短)</h2>

<p>“函数只做一件事”这句话听起来像句正确的空话，谁都会说。真正让我改主意的是书里那个判断标准：如果你能从这段代码里再拆出一个函数、并且新函数的名字不是对原函数的简单重复，那说明原函数确实干了不止一件事。</p>

<p>这个标准比”多少行以内”实用多了。我以前写一个处理流程，习惯把校验、算钱、存库、发通知全堆在一个函数里，中间穿插着注释分段。看着挺整齐，其实就是偷懒。现在这种活会拆成好几个小函数，主函数读起来跟看目录一样：</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">function</span> <span class="nx">register</span><span class="p">(</span><span class="nx">user</span><span class="p">:</span> <span class="nx">User</span><span class="p">):</span> <span class="k">void</span> <span class="p">{</span>
    <span class="nx">validate</span><span class="p">(</span><span class="nx">user</span><span class="p">);</span>
    <span class="nx">applyDiscount</span><span class="p">(</span><span class="nx">user</span><span class="p">);</span>
    <span class="nx">persist</span><span class="p">(</span><span class="nx">user</span><span class="p">);</span>
    <span class="nx">notify</span><span class="p">(</span><span class="nx">user</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>拆完之后有个意外的好处：写单测方便多了。以前那种大杂烩函数，写测试的时候要构造一堆前置条件才能覆盖到中间某一步，现在每个小函数单独测，轻松很多。</p>

<p>参数个数那条我倒是没完全照做，书里说超过两三个参数就该封装成对象，道理对，但很多时候懒得为一个用一次的场景专门建个类。</p>

<h2 id="注释这块我以前的理解是反的">注释这块，我以前的理解是反的</h2>

<p>我以前觉得注释多是负责任的表现，改完代码习惯性写一句”这里是干什么的”。看完这本书才意识到，这种注释基本等于承认代码没写清楚——如果命名和结构到位，代码本身就该讲清楚”做什么”，注释真正该干的事是解释”为什么”：为什么这里绕了个弯、为什么不能用更直接的写法。</p>

<p>过期注释更让人难受。我确实写过那种改了代码却没改注释的情况，后来的人（包括我自己）直接信了注释,结果被坑。看完这段之后，我现在宁可少写注释，也不留一句可能过期的说明。</p>

<p>被注释掉的死代码也一样，以前总舍不得删，”说不定以后用得上”，现在想开了，有版本控制记着呢，删就删。</p>

<h2 id="对象和数据结构这条彻底刷新了我的理解">对象和数据结构，这条彻底刷新了我的理解</h2>

<p>这一章我看了两遍才真的转过弯。之前一直觉得”面向对象”就是把数据和方法包一起，写着写着就写出一堆四不像的类——公开一堆字段又挂几个方法，两头都不占。</p>

<p>书里说得很直接：对象应该隐藏数据、暴露行为；数据结构应该反过来，暴露数据、没有行为。</p>

<p>混着写，两边的好处都拿不到。这条思路帮我理清了不少之前设计不清楚的类，遇到只是拿来传数据的场景，我现在会大方地让它只是个数据结构，不硬塞方法进去。</p>

<p>得墨忒耳定律（最少知识原则）也是这章里让我印象深的，说白了就是别写 <code class="language-plaintext highlighter-rouge">a.getB().getC().doSomething()</code> 这种链式穿透。这种写法我以前写起来还挺顺手，觉得省事，现在看就是把内部结构暴露得一览无余，牵一发动全身。</p>

<h2 id="错误处理别返回-null">错误处理，别返回 null</h2>

<p>用异常代替返回码这条我一直是认同的，倒不算新知识。真正让我改变做法的是”别返回 null，也别传 null”这条。</p>

<p>我以前写方法，查不到东西习惯性 <code class="language-plaintext highlighter-rouge">return null</code>，调用的地方各自加判断。问题是总有人忘了判断，<code class="language-plaintext highlighter-rouge">NullPointerException</code> 就是这么来的，而且往往炸在离出错原因很远的地方，排查半天。现在会尽量返回空对象或者直接抛异常，让调用方不用猜”这里会不会是 null”。</p>

<h2 id="单测这件事从应该写变成顺手写">单测这件事，从”应该写”变成”顺手写”</h2>

<p>以前对单测的态度是”知道该写，但总没时间”。这本书没有讲什么新道理，但那句”测试代码和生产代码同等重要”这句话，配合前面函数拆小的习惯一起看，让我真正觉得写测试这件事门槛低了很多——函数一旦拆小、职责单一，测试自然就好写了，不再是一件额外的苦活。</p>

<h2 id="简单设计四规则短到我第一遍差点漏读">简单设计四规则，短到我第一遍差点漏读</h2>

<p>有一章特别短，短到我第一次读的时候几乎没留下印象，当成过渡章节就翻过去了。后来才反应过来，Kent Beck 那套”简单设计四规则”其实到处都有人引用，只是我当时没往心里去。</p>

<p>四条规则，是按优先级排的：</p>

<ol>
  <li>通过所有测试</li>
  <li>不包含重复代码</li>
  <li>表达了程序员的意图</li>
  <li>尽量减少类和方法的数量</li>
</ol>

<p>一开始看这四条觉得又是正确的空话，后来发现顺序才是关键：第一条摆在最前面，意思是先把测试跑通、把行为锁死，剩下三条才是在测试保护下才敢动手做的事——没有测试兜底,谁敢一边重构一边保证没改坏东西。</p>

<p>第二、第三条我平时不知不觉就在做，重复代码看到就想提取，命名不清楚就想改，这些跟前面几章讲的其实是一回事。真正让我多想了一下的是第四条,”尽量减少类和方法的数量”和第一章那种”拆函数、拆类”的建议摆在一起看，其实是有张力的：拆得太狠，元素一多，理解成本也上去了。书里把它排在最后，意思也很明白——先满足前三条,元素数量才是最后才该权衡的事,不能为了少写几个类,反过来牺牲表达力和去重。</p>

<h2 id="没完全被说服的地方">没完全被说服的地方</h2>

<p>这本书也不是每条都让我心服口服。比如格式那一章讲垂直对齐、水平间距的一些细节，现在基本都靠 formatter 自动搞定了，专门花心思去手动对齐感觉意义不大。系统与并发那一章讲得比较抽象，看完印象不深，可能是我自己实际接触并发场景不算多，共鸣有限。</p>

<p>坏味道那一章列的清单挺实用，但说实话看完没记住几条具体名字，更多是变成了一种”看到重复代码/长函数/多参数就浑身不舒服”的直觉，具体叫什么”坏味道”倒没那么重要。</p>

<h2 id="写在最后">写在最后</h2>

<p>这本书对我最大的作用，不是教会我什么新概念——里面大部分道理，稍微写过几年代码的人多少都听过。它真正做的是把这些散落的常识，用一套具体、可执行的判断标准串起来，逼着你去对照自己写的代码,一条一条地脸红。</p>

<p>如果只让我留一句话提醒自己，还是那句童子军军规：离开时，比来的时候更干净一点就够了。不用等一个”重构专项”，也不用等”有空的时候”，就是这一次改动，顺手做一点。</p>]]></content><author><name>bitgong</name></author><category term="博客文章" /><category term="代码整洁之道" /><category term="读书笔记" /><category term="代码质量" /><category term="编程实践" /><summary type="html"><![CDATA[《代码整洁之道》我读过5遍以上。这篇文章记录最让我对照自己代码脸红的几个判断：童子军军规、可搜索的命名、只做一件事的函数、解释「为什么」的注释、对象与数据结构的边界、不返回null，以及简单设计四规则的优先级顺序。它不教新概念，而是把常识用可执行的标准串起来。]]></summary></entry><entry><title type="html">bit-framework：把游戏框架拆成按需安装的模块</title><link href="https://me.bitgong.cn/posts/bit-framework%E6%A1%86%E6%9E%B6%E6%80%BB%E8%A7%88/" rel="alternate" type="text/html" title="bit-framework：把游戏框架拆成按需安装的模块" /><published>2026-08-19T02:00:00+00:00</published><updated>2026-08-19T02:00:00+00:00</updated><id>https://me.bitgong.cn/posts/bit-framework%E6%A1%86%E6%9E%B6%E6%80%BB%E8%A7%88</id><content type="html" xml:base="https://me.bitgong.cn/posts/bit-framework%E6%A1%86%E6%9E%B6%E6%80%BB%E8%A7%88/"><![CDATA[<h1 id="bit-framework把游戏框架拆成按需安装的模块">bit-framework：把游戏框架拆成按需安装的模块</h1>

<h2 id="为什么要重新做一遍">为什么要重新做一遍</h2>

<p>我之前维护过一个叫 kunpocc 的框架，UI 管理、平台适配、计时器、条件系统等功能全塞在一个包里。刚开始用着挺爽，装一个包什么都有。但用久了问题就冒出来了：</p>

<p>有的项目只想要个网络库，结果因为依赖了 kunpocc 主包，把 UI 系统、热更新那一堆代码也一起打进去了。想升级某个模块的一个小 bug，整个框架的版本号跟着往上跳，其他没改动的功能也要跟着走一遍回归测试。想单独维护 ECS 这部分逻辑，翻源码的时候还要绕开一堆不相关的目录。</p>

<p>说白了，单体框架在项目小的时候是效率，项目一多、维护周期一长，就变成了负担。所以这次没有缝缝补补，直接推翻重做：<strong>bit-framework</strong>，一个 pnpm Monorepo，把原来揉在一起的功能拆成 12 个完全独立的包，各自发版本、各自管依赖，你的项目需要什么就装什么。</p>

<h2 id="是什么">是什么</h2>

<p>bit-framework 是基于 Cocos Creator 3.x 的游戏开发框架集合，用 pnpm workspace 统一管理源码，但每个模块单独发到 npm，命名空间是 <code class="language-plaintext highlighter-rouge">@gongxh/bit-xxx</code>。12 个模块按功能分成 5 类：</p>

<table>
  <thead>
    <tr>
      <th>分类</th>
      <th>模块</th>
      <th>一句话</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>核心</td>
      <td>bit-core</td>
      <td>时间、定时器、平台检测、屏幕适配等基础工具</td>
    </tr>
    <tr>
      <td>UI</td>
      <td>bit-ui</td>
      <td>基于 FairyGUI 的窗口管理系统</td>
    </tr>
    <tr>
      <td>UI</td>
      <td>bit-condition</td>
      <td>条件显示系统，做红点、解锁提示</td>
    </tr>
    <tr>
      <td>游戏架构</td>
      <td>bit-ecs</td>
      <td>高性能 ECS 架构</td>
    </tr>
    <tr>
      <td>游戏架构</td>
      <td>bit-ec</td>
      <td>面向 Cocos 场景的轻量 EC 架构</td>
    </tr>
    <tr>
      <td>游戏架构</td>
      <td>bit-event</td>
      <td>全局事件系统</td>
    </tr>
    <tr>
      <td>网络与资源</td>
      <td>bit-net</td>
      <td>HTTP + WebSocket 跨平台网络库</td>
    </tr>
    <tr>
      <td>网络与资源</td>
      <td>bit-assets</td>
      <td>资源加载管理，按批次卸载</td>
    </tr>
    <tr>
      <td>网络与资源</td>
      <td>bit-hotupdate</td>
      <td>热更新系统封装</td>
    </tr>
    <tr>
      <td>工具</td>
      <td>bit-quadtree</td>
      <td>四叉树空间索引，做碰撞检测</td>
    </tr>
    <tr>
      <td>工具</td>
      <td>bit-behaviortree</td>
      <td>行为树，做游戏 AI</td>
    </tr>
    <tr>
      <td>工具</td>
      <td>bit-minigame</td>
      <td>微信/支付宝/字节跳动小游戏平台适配</td>
    </tr>
  </tbody>
</table>

<p>每个模块都可以单独安装、单独使用，互相之间没有强制绑定。</p>

<h2 id="怎么设计的">怎么设计的</h2>

<p>拆模块最容易踩的坑是拆着拆着又变成一堆互相纠缠的小单体，所以这次定了几条硬规矩：</p>

<p><strong>单向依赖，禁止循环</strong>。模块之间只允许单向引用，比如 <code class="language-plaintext highlighter-rouge">bit-ui</code> 依赖 <code class="language-plaintext highlighter-rouge">bit-core</code>，但 <code class="language-plaintext highlighter-rouge">bit-core</code> 绝对不会反过来依赖 <code class="language-plaintext highlighter-rouge">bit-ui</code>。真的需要双向通信的场景（比如 UI 层要感知业务层的状态变化），走 <code class="language-plaintext highlighter-rouge">bit-event</code> 广播，不走模块间直接调用。</p>

<p><strong>该依赖的东西交给 peer dependency</strong>，由使用方的项目自己装，保证全项目只有一份实例。比如 <code class="language-plaintext highlighter-rouge">bit-ui</code> 需要 <code class="language-plaintext highlighter-rouge">bit-core</code> 和 <code class="language-plaintext highlighter-rouge">@gongxh/fairygui-cc</code>，但这两个包不会被 <code class="language-plaintext highlighter-rouge">bit-ui</code> 自己打进去，而是声明成 peer 依赖：</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>npm <span class="nb">install</span> @gongxh/bit-ui @gongxh/bit-core @gongxh/fairygui-cc
</code></pre></div></div>

<p>依赖关系整体是这样的：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>bit-core  ←── bit-ui / bit-condition / bit-minigame / bit-hotupdate
bit-net   ←── bit-hotupdate
bit-event ←── bit-ec

bit-ecs / bit-assets / bit-quadtree / bit-behaviortree  完全独立，零依赖
</code></pre></div></div>

<p>也就是说，如果你只想要一个四叉树碰撞检测，或者只想要个行为树做 AI，装一个包就够了，不会因此把整套 UI 系统也带进项目。</p>

<h2 id="怎么用">怎么用</h2>

<p><strong>方式一：在你的 Cocos Creator 项目里按需装</strong>（推荐）</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 只要核心工具</span>
npm <span class="nb">install</span> @gongxh/bit-core

<span class="c"># UI 系统（peer 依赖要一起装）</span>
npm <span class="nb">install</span> @gongxh/bit-ui @gongxh/bit-core @gongxh/fairygui-cc

<span class="c"># 一次装个常用组合</span>
npm <span class="nb">install</span> @gongxh/bit-core @gongxh/bit-ui @gongxh/bit-event @gongxh/bit-net @gongxh/fairygui-cc
</code></pre></div></div>

<p><strong>方式二：本地开发/参与贡献</strong></p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git clone <span class="nt">--recurse-submodules</span> https://github.com/gongxh0901/bit-framework.git
<span class="nb">cd </span>bit-framework
pnpm <span class="nb">install
</span>pnpm build
</code></pre></div></div>

<p>用 pnpm 而不是 npm/yarn，主要是看中它的硬链接省磁盘、依赖管理更严格、原生支持 workspace，几个包一起改动的时候体验明显更顺。</p>

<h2 id="技术栈">技术栈</h2>

<p>TypeScript 5.x 开发，Rollup 打包，每个模块的 <code class="language-plaintext highlighter-rouge">dist/</code> 下都会产出 ESM、CommonJS 和压缩版，外加完整的 <code class="language-plaintext highlighter-rouge">.d.ts</code> 类型定义。装饰器用得比较多（<code class="language-plaintext highlighter-rouge">experimentalDecorators</code>），UI、ECS、EC、条件系统都是靠装饰器注册类的。Cocos Creator 版本要求 3.7.0+，日常开发用的是 3.8.x。</p>

<h2 id="后续会写什么">后续会写什么</h2>

<p>这篇是全局导览，接下来会给每个模块单独写一篇，讲清楚它解决什么问题、核心 API 怎么用、有哪些坑要避开。如果你只关心某一块功能，可以直接跳过去看对应模块的文章。</p>

<h2 id="项目信息">项目信息</h2>

<ul>
  <li><strong>GitHub</strong>: <a href="https://github.com/gongxh0901/bit-framework">https://github.com/gongxh0901/bit-framework</a></li>
  <li><strong>许可证</strong>: MIT License</li>
  <li><strong>作者</strong>: bit老宫 (gongxh)</li>
</ul>

<p>（下面各模块文章中的仓库链接会指向对应子目录，方便直接跳转看源码。）</p>

<p>欢迎按需取用，也欢迎提 Issue 和 PR。</p>]]></content><author><name>bitgong</name></author><category term="开源项目" /><category term="Cocos Creator" /><category term="TypeScript" /><category term="Monorepo" /><category term="游戏框架" /><category term="开源" /><summary type="html"><![CDATA[基于 pnpm Monorepo 的 Cocos Creator 3.x 游戏框架集合，拆成 12 个独立模块按需安装，覆盖 UI、ECS、网络、资源、热更新全流程。]]></summary></entry><entry><title type="html">bit-core：拆出来的基础工具库，其他模块都靠它兜底</title><link href="https://me.bitgong.cn/posts/bit-core%E6%A0%B8%E5%BF%83%E5%BA%93/" rel="alternate" type="text/html" title="bit-core：拆出来的基础工具库，其他模块都靠它兜底" /><published>2026-08-18T02:00:00+00:00</published><updated>2026-08-18T02:00:00+00:00</updated><id>https://me.bitgong.cn/posts/bit-core%E6%A0%B8%E5%BF%83%E5%BA%93</id><content type="html" xml:base="https://me.bitgong.cn/posts/bit-core%E6%A0%B8%E5%BF%83%E5%BA%93/"><![CDATA[<h1 id="bit-core拆出来的基础工具库其他模块都靠它兜底">bit-core：拆出来的基础工具库，其他模块都靠它兜底</h1>

<h2 id="它解决什么问题">它解决什么问题</h2>

<p>写 Cocos Creator 游戏的时候，有一堆代码几乎每个项目都要写一遍：判断当前是不是微信小游戏、拿屏幕安全区做适配、搞个全局定时器管技能冷却、格式化一个倒计时文案……这些东西单独看都不难，但项目多了之后，同样的逻辑在不同项目里重复实现，风格还都不一样，改一次 bug 只改了一个项目，其他项目还带着老问题。</p>

<p>bit-core 就是把这些”每个项目都要写”的基础功能收拢到一个包里：时间处理、定时器、平台检测、屏幕适配、日志、常用数据结构。它是 bit-framework 里被依赖最多的模块，<code class="language-plaintext highlighter-rouge">bit-ui</code>、<code class="language-plaintext highlighter-rouge">bit-condition</code>、<code class="language-plaintext highlighter-rouge">bit-minigame</code>、<code class="language-plaintext highlighter-rouge">bit-hotupdate</code> 都把它当 peer 依赖。</p>

<h2 id="安装">安装</h2>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>npm <span class="nb">install</span> @gongxh/bit-core
</code></pre></div></div>

<p>不需要任何其他依赖，可以独立使用。</p>

<h2 id="主要能力">主要能力</h2>

<h3 id="时间工具time">时间工具（Time）</h3>

<p>除了拿时间戳这种基础操作，比较实用的是内置了网络时间同步——很多游戏对本地时间不完全信任（怕玩家改系统时间作弊），可以把服务器时间同步进来做基准：</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">Time</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">@gongxh/bit-core</span><span class="dl">'</span><span class="p">;</span>

<span class="nx">Time</span><span class="p">.</span><span class="nx">setNetTime</span><span class="p">(</span><span class="nx">serverTimestamp</span><span class="p">);</span> <span class="c1">// 用服务器时间校正</span>
<span class="nx">Time</span><span class="p">.</span><span class="nx">now</span><span class="p">();</span> <span class="c1">// 之后拿到的都是校正后的时间</span>

<span class="nx">Time</span><span class="p">.</span><span class="nx">getDayStartTime</span><span class="p">();</span> <span class="c1">// 当天 0 点时间戳，结算日重置常用</span>
<span class="nx">Time</span><span class="p">.</span><span class="nx">isSameDay</span><span class="p">(</span><span class="nx">t1</span><span class="p">,</span> <span class="nx">t2</span><span class="p">);</span> <span class="c1">// 判断是否同一天</span>
</code></pre></div></div>

<p>时长格式化这块也做了两种粒度：<code class="language-plaintext highlighter-rouge">formatDuration</code> 按你给的 pattern 精确格式化，<code class="language-plaintext highlighter-rouge">formatSmart</code> / <code class="language-plaintext highlighter-rouge">formatSmartSimple</code> 会自动隐藏为 0 的单位，适合倒计时文案不想手动拼接的场景。</p>

<h3 id="全局定时器globaltimer">全局定时器（GlobalTimer）</h3>

<p>游戏里散落的 <code class="language-plaintext highlighter-rouge">setInterval</code>/<code class="language-plaintext highlighter-rouge">setTimeout</code> 最容易出的问题是场景切换时忘记清理，导致回调打在已经销毁的对象上。GlobalTimer 统一管理，还支持暂停恢复：</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">GlobalTimer</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">@gongxh/bit-core</span><span class="dl">'</span><span class="p">;</span>

<span class="c1">// 每秒执行一次，无限重复</span>
<span class="kd">const</span> <span class="nx">timerId</span> <span class="o">=</span> <span class="nx">GlobalTimer</span><span class="p">.</span><span class="nx">startTimer</span><span class="p">(()</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">心跳</span><span class="dl">'</span><span class="p">);</span>
<span class="p">},</span> <span class="mi">1</span><span class="p">,</span> <span class="o">-</span><span class="mi">1</span><span class="p">);</span>

<span class="nx">GlobalTimer</span><span class="p">.</span><span class="nx">pauseTimer</span><span class="p">(</span><span class="nx">timerId</span><span class="p">);</span>  <span class="c1">// 比如切到后台时暂停</span>
<span class="nx">GlobalTimer</span><span class="p">.</span><span class="nx">resumeTimer</span><span class="p">(</span><span class="nx">timerId</span><span class="p">);</span>
<span class="nx">GlobalTimer</span><span class="p">.</span><span class="nx">stopTimer</span><span class="p">(</span><span class="nx">timerId</span><span class="p">);</span>
</code></pre></div></div>

<h3 id="平台检测platform">平台检测（Platform）</h3>

<p>小游戏平台一多，<code class="language-plaintext highlighter-rouge">if-else</code> 判断平台的代码到处都是。Platform 把这些判断收成一批只读属性：</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">Platform</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">@gongxh/bit-core</span><span class="dl">'</span><span class="p">;</span>

<span class="k">if</span> <span class="p">(</span><span class="nx">Platform</span><span class="p">.</span><span class="nx">isWX</span><span class="p">)</span> <span class="p">{</span>
    <span class="c1">// 微信小游戏</span>
<span class="p">}</span> <span class="k">else</span> <span class="k">if</span> <span class="p">(</span><span class="nx">Platform</span><span class="p">.</span><span class="nx">isNativeMobile</span><span class="p">)</span> <span class="p">{</span>
    <span class="c1">// 原生移动端（Android/iOS/HarmonyOS）</span>
<span class="p">}</span>
</code></pre></div></div>

<h3 id="屏幕适配screen">屏幕适配（Screen）</h3>

<p>刘海屏、挖孔屏这些异形屏，安全区不处理好，UI 元素会被摄像头或者虚拟按键挡住。Screen 提供了拿到手就能用的安全区信息：</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">Screen</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">@gongxh/bit-core</span><span class="dl">'</span><span class="p">;</span>

<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">Screen</span><span class="p">.</span><span class="nx">SafeWidth</span><span class="p">,</span> <span class="nx">Screen</span><span class="p">.</span><span class="nx">SafeHeight</span><span class="p">);</span>   <span class="c1">// 安全区宽高</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">Screen</span><span class="p">.</span><span class="nx">SafeAreaTop</span><span class="p">,</span> <span class="nx">Screen</span><span class="p">.</span><span class="nx">SafeAreaBottom</span><span class="p">);</span> <span class="c1">// 四边 inset</span>
</code></pre></div></div>

<p>有个细节要注意：横屏因为没法区分设备是左手还是右手朝向，左右两侧的安全区会对称地都用 <code class="language-plaintext highlighter-rouge">safeAreaTop</code> 的配置值，不是分别计算。长边和短边比例小于等于 16:9 的设备（大多数平板），四边 inset 会直接清零，安全区等于全屏。</p>

<h3 id="日志系统">日志系统</h3>

<p>统一的日志输出，方便上线后统一关闭调试日志：</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">enableDebugMode</span><span class="p">,</span> <span class="nx">debug</span><span class="p">,</span> <span class="nx">warn</span><span class="p">,</span> <span class="nx">error</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">@gongxh/bit-core</span><span class="dl">'</span><span class="p">;</span>

<span class="nx">enableDebugMode</span><span class="p">(</span><span class="kc">false</span><span class="p">);</span> <span class="c1">// 生产环境关掉 debug 级别日志</span>
</code></pre></div></div>

<h3 id="数据结构">数据结构</h3>

<p>内置了二叉堆、链表、双向链表、栈，都是游戏开发里常会手写的东西，直接拿来用：</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">BinaryHeap</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">@gongxh/bit-core</span><span class="dl">'</span><span class="p">;</span>

<span class="c1">// 小顶堆，常用来做技能冷却队列、AI 决策优先级排序</span>
<span class="kd">const</span> <span class="nx">heap</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">BinaryHeap</span><span class="o">&lt;</span><span class="nx">Task</span><span class="o">&gt;</span><span class="p">((</span><span class="nx">a</span><span class="p">,</span> <span class="nx">b</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="nx">a</span><span class="p">.</span><span class="nx">priority</span> <span class="o">-</span> <span class="nx">b</span><span class="p">.</span><span class="nx">priority</span><span class="p">);</span>
<span class="nx">heap</span><span class="p">.</span><span class="nx">push</span><span class="p">(</span><span class="nx">task</span><span class="p">);</span>
<span class="nx">heap</span><span class="p">.</span><span class="nx">pop</span><span class="p">();</span>
</code></pre></div></div>

<h3 id="模块基类module">模块基类（Module）</h3>

<p>如果你的项目习惯把游戏系统拆成一个个”模块”（背包模块、任务模块……），可以继承 <code class="language-plaintext highlighter-rouge">Module</code> 统一生命周期入口，实现 <code class="language-plaintext highlighter-rouge">onInit()</code> 就行，不用每个系统都自己定义一套初始化规范。</p>

<h2 id="避坑提醒">避坑提醒</h2>

<ul>
  <li><code class="language-plaintext highlighter-rouge">Screen</code> 的安全区配置要在 <code class="language-plaintext highlighter-rouge">CocosEntry</code> 的 Inspector 面板里设，不是代码里传参数，容易漏配导致横屏适配不对。</li>
  <li><code class="language-plaintext highlighter-rouge">Time.setNetTime()</code> 只是给个基准，之后 <code class="language-plaintext highlighter-rouge">Time.now()</code> 是基于这个基准 + 本地流逝时间计算的，不是每次都重新请求服务器，别指望它帮你做防作弊校验，真要防作弊还是得后端兜底。</li>
  <li><code class="language-plaintext highlighter-rouge">GlobalTimer</code> 是全局单例，场景切换、组件销毁的时候记得手动 <code class="language-plaintext highlighter-rouge">stopTimer</code>，不然定时器会一直跑。</li>
</ul>

<h2 id="项目信息">项目信息</h2>

<ul>
  <li><strong>GitHub</strong>: <a href="https://github.com/gongxh0901/bit-framework/tree/main/bit-core">https://github.com/gongxh0901/bit-framework/tree/main/bit-core</a></li>
  <li><strong>npm</strong>: <a href="https://www.npmjs.com/package/@gongxh/bit-core">@gongxh/bit-core</a></li>
  <li><strong>许可证</strong>: MIT License</li>
</ul>

<p>如果你的项目需要 UI 管理（<code class="language-plaintext highlighter-rouge">bit-ui</code>）、条件显示（<code class="language-plaintext highlighter-rouge">bit-condition</code>）、小游戏适配（<code class="language-plaintext highlighter-rouge">bit-minigame</code>）或热更新（<code class="language-plaintext highlighter-rouge">bit-hotupdate</code>），这几个模块都是直接把 bit-core 当 peer 依赖用的，装它们的时候记得把 bit-core 也一起装上。</p>]]></content><author><name>bitgong</name></author><category term="开源项目" /><category term="Cocos Creator" /><category term="TypeScript" /><category term="开源" /><category term="工具库" /><summary type="html"><![CDATA[bit-framework 的核心工具库，提供时间处理、全局定时器、平台检测、屏幕适配、日志和常用数据结构，是多个模块的 peer 依赖。]]></summary></entry><entry><title type="html">bit-ui：基于 FairyGUI 的窗口管理，装饰器一顿标注就能用</title><link href="https://me.bitgong.cn/posts/bit-ui%E7%95%8C%E9%9D%A2%E7%AE%A1%E7%90%86/" rel="alternate" type="text/html" title="bit-ui：基于 FairyGUI 的窗口管理，装饰器一顿标注就能用" /><published>2026-08-17T02:00:00+00:00</published><updated>2026-08-17T02:00:00+00:00</updated><id>https://me.bitgong.cn/posts/bit-ui%E7%95%8C%E9%9D%A2%E7%AE%A1%E7%90%86</id><content type="html" xml:base="https://me.bitgong.cn/posts/bit-ui%E7%95%8C%E9%9D%A2%E7%AE%A1%E7%90%86/"><![CDATA[<h1 id="bit-ui基于-fairygui-的窗口管理装饰器一顿标注就能用">bit-ui：基于 FairyGUI 的窗口管理，装饰器一顿标注就能用</h1>

<h2 id="它解决什么问题">它解决什么问题</h2>

<p>用 FairyGUI 做 UI 的项目，早晚都要自己写一套窗口管理：打开窗口要不要先关掉上一个、UI 资源什么时候加载什么时候释放、多个窗口叠在一起谁在最上层、切场景的时候一堆没关掉的窗口怎么清理干净。这些逻辑不是难，是琐碎，而且每个项目都要重新写一遍，还容易漏处理某个边界情况（比如窗口没显示完就被销毁，资源引用计数对不上）。</p>

<p>bit-ui 把这套窗口生命周期管理做成了现成的框架，你只要按装饰器的规则写窗口类，剩下资源加载、层级、多窗口组的事交给它。</p>

<h2 id="安装">安装</h2>

<p><code class="language-plaintext highlighter-rouge">bit-core</code> 和 <code class="language-plaintext highlighter-rouge">@gongxh/fairygui-cc</code> 是 peer 依赖，要一起装，保证全项目只有一份实例：</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>npm <span class="nb">install</span> @gongxh/bit-ui @gongxh/bit-core @gongxh/fairygui-cc
</code></pre></div></div>

<h2 id="核心用法">核心用法</h2>

<h3 id="定义一个窗口">定义一个窗口</h3>

<p>窗口类继承 <code class="language-plaintext highlighter-rouge">Window</code>，用装饰器注册：</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">Window</span><span class="p">,</span> <span class="nx">_uidecorator</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">@gongxh/bit-ui</span><span class="dl">'</span><span class="p">;</span>
<span class="kd">const</span> <span class="p">{</span> <span class="nx">uiclass</span><span class="p">,</span> <span class="nx">uiprop</span><span class="p">,</span> <span class="nx">uiclick</span> <span class="p">}</span> <span class="o">=</span> <span class="nx">_uidecorator</span><span class="p">;</span>

<span class="p">@</span><span class="nd">uiclass</span><span class="p">(</span><span class="dl">"</span><span class="s2">Home</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">MainWindow</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">MainWindow</span><span class="dl">"</span><span class="p">)</span>
<span class="k">export</span> <span class="kd">class</span> <span class="nx">MainWindow</span> <span class="kd">extends</span> <span class="nx">Window</span> <span class="p">{</span>
    <span class="p">@</span><span class="nd">uiprop</span> <span class="k">private</span> <span class="nx">btnStart</span><span class="p">:</span> <span class="nx">GButton</span><span class="p">;</span>
    <span class="p">@</span><span class="nd">uiprop</span> <span class="k">private</span> <span class="nx">txtTitle</span><span class="p">:</span> <span class="nx">GTextField</span><span class="p">;</span>

    <span class="k">protected</span> <span class="nx">onShow</span><span class="p">(</span><span class="nx">userdata</span><span class="p">?:</span> <span class="kr">any</span><span class="p">):</span> <span class="k">void</span> <span class="p">{</span>
        <span class="k">this</span><span class="p">.</span><span class="nx">txtTitle</span><span class="p">.</span><span class="nx">text</span> <span class="o">=</span> <span class="nx">userdata</span><span class="p">?.</span><span class="nx">title</span> <span class="o">??</span> <span class="dl">'</span><span class="s1">默认标题</span><span class="dl">'</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="p">@</span><span class="nd">uiclick</span>
    <span class="k">private</span> <span class="nx">onBtnStart</span><span class="p">():</span> <span class="k">void</span> <span class="p">{</span>
        <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">开始游戏</span><span class="dl">'</span><span class="p">);</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">uiclass</code> 的三个参数分别是窗口组名称、FairyGUI 包名、组件名——组件名必须和类名完全一致，这是它能自动关联 FairyGUI 资源的关键，写错了会找不到对应组件。</p>

<h3 id="打开关闭窗口">打开/关闭窗口</h3>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">WindowManager</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">@gongxh/bit-ui</span><span class="dl">'</span><span class="p">;</span>

<span class="c1">// 参数是窗口类本身，不是字符串名称</span>
<span class="k">await</span> <span class="nx">WindowManager</span><span class="p">.</span><span class="nx">showWindow</span><span class="p">(</span><span class="nx">MainWindow</span><span class="p">,</span> <span class="p">{</span> <span class="na">title</span><span class="p">:</span> <span class="dl">'</span><span class="s1">欢迎回来</span><span class="dl">'</span> <span class="p">});</span>

<span class="nx">WindowManager</span><span class="p">.</span><span class="nx">closeWindow</span><span class="p">(</span><span class="nx">MainWindow</span><span class="p">);</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">showWindow</code> 是异步的，因为它内部要先把这个窗口对应的 FairyGUI 包加载进来，加载完了才创建、显示。如果打开窗口前还需要额外请求服务器数据（比如打开背包前先拉一次背包列表接口），可以用 <code class="language-plaintext highlighter-rouge">beforeLoad</code>，这段逻辑会在 UI 包加载前先执行完：</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">await</span> <span class="nx">WindowManager</span><span class="p">.</span><span class="nx">showWindow</span><span class="p">(</span><span class="nx">BagWindow</span><span class="p">,</span> <span class="nx">userdata</span><span class="p">,</span> <span class="p">{</span>
    <span class="na">beforeLoad</span><span class="p">:</span> <span class="k">async</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
        <span class="nx">userdata</span><span class="p">.</span><span class="nx">bagList</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">requestBagList</span><span class="p">();</span>
    <span class="p">},</span>
<span class="p">});</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">beforeLoad</code> 执行期间会复用你配置的通用等待窗，不用自己再单独弹一个 loading。</p>

<h3 id="窗口之间的关系">窗口之间的关系</h3>

<p>打开新窗口时，<code class="language-plaintext highlighter-rouge">WindowType</code> 决定怎么处理原来在屏幕上的窗口：</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">Normal</code> —— 不做任何处理，叠在上面</li>
  <li><code class="language-plaintext highlighter-rouge">CloseOne</code> / <code class="language-plaintext highlighter-rouge">HideOne</code> —— 关闭/隐藏上一个窗口</li>
  <li><code class="language-plaintext highlighter-rouge">CloseAll</code> / <code class="language-plaintext highlighter-rouge">HideAll</code> —— 关闭/隐藏所有窗口</li>
</ul>

<p>比如从主界面进副本界面，通常想把主界面隐藏而不是关闭（回来的时候能直接恢复状态），这时候用 <code class="language-plaintext highlighter-rouge">HideOne</code>，副本界面关闭后主界面会自动走 <code class="language-plaintext highlighter-rouge">onShowFromHide()</code> 生命周期恢复回来。</p>

<h3 id="header-复用">Header 复用</h3>

<p>如果好几个窗口顶部都是同一条资源栏（金币、钻石、体力），没必要每个窗口都重复放一份，用 <code class="language-plaintext highlighter-rouge">Header</code> 基类单独定义一次，跨窗口复用：</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">Header</span><span class="p">,</span> <span class="nx">_uidecorator</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">@gongxh/bit-ui</span><span class="dl">'</span><span class="p">;</span>
<span class="kd">const</span> <span class="p">{</span> <span class="nx">uiheader</span> <span class="p">}</span> <span class="o">=</span> <span class="nx">_uidecorator</span><span class="p">;</span>

<span class="p">@</span><span class="nd">uiheader</span><span class="p">(</span><span class="dl">"</span><span class="s2">Common</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">TopHeader</span><span class="dl">"</span><span class="p">)</span>
<span class="k">export</span> <span class="kd">class</span> <span class="nx">TopHeader</span> <span class="kd">extends</span> <span class="nx">Header</span> <span class="p">{</span>
    <span class="k">protected</span> <span class="nx">onShow</span><span class="p">(</span><span class="nx">userdata</span><span class="p">?:</span> <span class="kr">any</span><span class="p">):</span> <span class="k">void</span> <span class="p">{</span>
        <span class="c1">// 刷新金币、钻石显示</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<h3 id="适配类型">适配类型</h3>

<p>窗口用 <code class="language-plaintext highlighter-rouge">AdapterType</code> 决定怎么适配屏幕：</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">Full</code> —— 全屏适配（默认，大部分窗口用这个）</li>
  <li><code class="language-plaintext highlighter-rouge">Bang</code> —— 按 <code class="language-plaintext highlighter-rouge">Screen</code>（来自 bit-core）算出的安全区避让，窗口定位在安全区中心，适合异形屏上不想被摄像头/挖孔挡住的窗口</li>
  <li><code class="language-plaintext highlighter-rouge">Fixed</code> —— 固定尺寸不适配，适合弹窗、提示框这类</li>
</ul>

<h2 id="避坑提醒">避坑提醒</h2>

<ul>
  <li><code class="language-plaintext highlighter-rouge">uiclass</code> 第三个参数（组件名）必须和 TypeScript 类名一致，这是隐性约定，不遵守的话运行时会提示找不到组件，排查起来容易懵。</li>
  <li><code class="language-plaintext highlighter-rouge">showWindow</code> 传的是窗口类本身（构造函数），不是窗口名字符串，跟很多其他 UI 框架的习惯不一样，容易写错。</li>
  <li>用 <code class="language-plaintext highlighter-rouge">HideOne</code>/<code class="language-plaintext highlighter-rouge">HideAll</code> 隐藏的窗口不会自动释放资源，只有 <code class="language-plaintext highlighter-rouge">closeWindow</code> 才会走完整的生命周期和资源回收，长期隐藏不用的窗口记得该关就关。</li>
  <li>配套的可视化编辑器 <a href="https://store.cocos.com/app/detail/7213">kunpo-fgui</a> 是付费插件，能一键导出 FairyGUI 配置省去手写绑定代码，不是必须，但界面多的项目用它能省不少事。</li>
</ul>

<h2 id="项目信息">项目信息</h2>

<ul>
  <li><strong>GitHub</strong>: <a href="https://github.com/gongxh0901/bit-framework/tree/main/bit-ui">https://github.com/gongxh0901/bit-framework/tree/main/bit-ui</a></li>
  <li><strong>npm</strong>: <a href="https://www.npmjs.com/package/@gongxh/bit-ui">@gongxh/bit-ui</a></li>
  <li><strong>许可证</strong>: MIT License</li>
</ul>

<p>需要红点、功能解锁提示这类跟 UI 联动的条件显示逻辑，可以配合 <code class="language-plaintext highlighter-rouge">bit-condition</code> 一起用。</p>]]></content><author><name>bitgong</name></author><category term="开源项目" /><category term="Cocos Creator" /><category term="FairyGUI" /><category term="TypeScript" /><category term="开源" /><category term="UI框架" /><summary type="html"><![CDATA[基于 FairyGUI 的 Cocos Creator UI 管理库，装饰器注册窗口、自动加载卸载资源、多窗口组管理，配套可视化编辑器一键导出配置。]]></summary></entry><entry><title type="html">bit-condition：红点该不该显示，交给条件系统自动算</title><link href="https://me.bitgong.cn/posts/bit-condition%E6%9D%A1%E4%BB%B6%E6%98%BE%E7%A4%BA/" rel="alternate" type="text/html" title="bit-condition：红点该不该显示，交给条件系统自动算" /><published>2026-08-16T02:00:00+00:00</published><updated>2026-08-16T02:00:00+00:00</updated><id>https://me.bitgong.cn/posts/bit-condition%E6%9D%A1%E4%BB%B6%E6%98%BE%E7%A4%BA</id><content type="html" xml:base="https://me.bitgong.cn/posts/bit-condition%E6%9D%A1%E4%BB%B6%E6%98%BE%E7%A4%BA/"><![CDATA[<h1 id="bit-condition红点该不该显示交给条件系统自动算">bit-condition：红点该不该显示，交给条件系统自动算</h1>

<h2 id="它解决什么问题">它解决什么问题</h2>

<p>红点系统看着简单，做起来烦。一个红点的显示条件可能是”有未读邮件”，另一个可能是”背包里有新装备 或者 有可升级的装备”，条件组合一多，代码里到处散落着 <code class="language-plaintext highlighter-rouge">if (hasNewMail() || hasUpgradableItem())</code> 这种判断，而且数据一变还要记得手动去刷新对应的红点节点，漏刷新是最常见的红点 bug 来源。</p>

<p>bit-condition 把”判断条件”和”通知 UI 更新”这两件事拆开管理：你只管写条件类怎么判断，节点怎么显示交给框架，数据变化时通知框架重新算一遍，该显示的自动显示，该隐藏的自动隐藏。</p>

<h2 id="安装">安装</h2>

<p><code class="language-plaintext highlighter-rouge">bit-core</code> 和 <code class="language-plaintext highlighter-rouge">@gongxh/fairygui-cc</code> 是 peer 依赖：</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>npm <span class="nb">install</span> @gongxh/bit-condition @gongxh/bit-core @gongxh/fairygui-cc
</code></pre></div></div>

<h2 id="核心用法">核心用法</h2>

<h3 id="1-加条件模块">1. 加条件模块</h3>

<p>场景根节点或者一个常驻的管理节点上挂 <code class="language-plaintext highlighter-rouge">ConditionModule</code> 组件，它负责定时批量重算所有待更新的条件（默认 0.3 秒一次），不是每次数据变化就立刻算，攒一批一起处理，性能更友好：</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// 挂在场景管理节点上，updateDeltaTime 可以在 Inspector 里调</span>
</code></pre></div></div>

<h3 id="2-定义条件类型和条件类">2. 定义条件类型和条件类</h3>

<p>先用枚举定义好有哪些条件类型，再继承 <code class="language-plaintext highlighter-rouge">ConditionBase</code> 实现判断逻辑：</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">_conditionDecorator</span><span class="p">,</span> <span class="nx">ConditionBase</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">@gongxh/bit-condition</span><span class="dl">'</span><span class="p">;</span>

<span class="kr">enum</span> <span class="nx">ConditionType</span> <span class="p">{</span>
    <span class="nx">NewMail</span><span class="p">,</span>
    <span class="nx">UpgradableItem</span><span class="p">,</span>
<span class="p">}</span>

<span class="p">@</span><span class="nd">_conditionDecorator</span><span class="p">.</span><span class="nx">conditionClass</span><span class="p">(</span><span class="nx">ConditionType</span><span class="p">.</span><span class="nx">NewMail</span><span class="p">)</span>
<span class="k">export</span> <span class="kd">class</span> <span class="nx">NewMailCondition</span> <span class="kd">extends</span> <span class="nx">ConditionBase</span> <span class="p">{</span>
    <span class="k">protected</span> <span class="nx">onInit</span><span class="p">():</span> <span class="k">void</span> <span class="p">{</span>
        <span class="c1">// 这里可以注册数据变化的监听</span>
    <span class="p">}</span>

    <span class="k">protected</span> <span class="nx">evaluate</span><span class="p">():</span> <span class="nx">boolean</span> <span class="p">{</span>
        <span class="k">return</span> <span class="nx">MailSystem</span><span class="p">.</span><span class="nx">hasUnreadMail</span><span class="p">();</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">evaluate()</code> 只管返回布尔值，什么时候调用、调用完怎么通知节点，都不用你操心。</p>

<h3 id="3-数据变化时喊一声">3. 数据变化时喊一声</h3>

<p>邮件系统收到新邮件时，找到条件实例调用 <code class="language-plaintext highlighter-rouge">tryUpdate()</code>，告诉框架”我这个条件可能变了，下个周期重新算一下”：</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">newMailCondition</span><span class="p">.</span><span class="nx">tryUpdate</span><span class="p">();</span>
</code></pre></div></div>

<h3 id="4-挂红点节点">4. 挂红点节点</h3>

<p><code class="language-plaintext highlighter-rouge">ConditionAnyNode</code> 和 <code class="language-plaintext highlighter-rouge">ConditionAllNode</code> 是内置的两种组合模式，分别对应”任一条件满足就显示”和”所有条件都满足才显示”：</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">ConditionAnyNode</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">@gongxh/bit-condition</span><span class="dl">'</span><span class="p">;</span>

<span class="c1">// 邮件红点：有未读邮件 或者 有可升级装备，任一满足就亮红点</span>
<span class="kd">const</span> <span class="nx">redDot</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">ConditionAnyNode</span><span class="p">(</span><span class="nx">redDotNode</span><span class="p">,</span> <span class="nx">ConditionType</span><span class="p">.</span><span class="nx">NewMail</span><span class="p">,</span> <span class="nx">ConditionType</span><span class="p">.</span><span class="nx">UpgradableItem</span><span class="p">);</span>
</code></pre></div></div>

<p>节点销毁前记得调用 <code class="language-plaintext highlighter-rouge">destroy()</code> 解绑；如果是挂在 FairyGUI 对象上的 <code class="language-plaintext highlighter-rouge">ConditionFGUINode</code>，<code class="language-plaintext highlighter-rouge">GObject.removeFromParent()</code> 的时候会自动帮你解绑，不用手动处理。</p>

<h2 id="条件组合模式">条件组合模式</h2>

<p><code class="language-plaintext highlighter-rouge">ConditionMode</code> 只有两种：</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">Any</code>（0）—— 任意一个条件满足就算满足，红点、提示类场景最常用</li>
  <li><code class="language-plaintext highlighter-rouge">All</code>（1）—— 所有条件都要满足，比如某个功能同时要求”等级达到10级”和”完成新手引导”才解锁</li>
</ul>

<p>如果 Any/All 都不够表达你的逻辑（比如条件之间还要加权重、或者要 A 且非 B），直接继承 <code class="language-plaintext highlighter-rouge">ConditionBase</code> 自己写一个组合条件类，<code class="language-plaintext highlighter-rouge">evaluate()</code> 里调用其他条件实例的结果自己拼逻辑就行，框架没有限制你必须用内置的两种模式。</p>

<h2 id="避坑提醒">避坑提醒</h2>

<ul>
  <li><code class="language-plaintext highlighter-rouge">evaluate()</code> 不会自动被高频调用，只有调用过 <code class="language-plaintext highlighter-rouge">tryUpdate()</code> 之后，才会在下一个更新周期被重新计算。如果条件判断依赖的数据变了但忘了调 <code class="language-plaintext highlighter-rouge">tryUpdate()</code>，红点状态就会一直停留在旧值，这是最容易踩的坑。</li>
  <li><code class="language-plaintext highlighter-rouge">ConditionModule</code> 只能挂一份在场景里生效，别指望多挂几个能加速刷新——批量定时更新本身就是为了避免频繁计算，加实例只会重复算,不会更快。</li>
  <li>条件类型枚举建议一次性统一定义好，散落在各个业务模块里定义容易和其他系统的枚举撞值。</li>
</ul>

<h2 id="项目信息">项目信息</h2>

<ul>
  <li><strong>GitHub</strong>: <a href="https://github.com/gongxh0901/bit-framework/tree/main/bit-condition">https://github.com/gongxh0901/bit-framework/tree/main/bit-condition</a></li>
  <li><strong>npm</strong>: <a href="https://www.npmjs.com/package/@gongxh/bit-condition">@gongxh/bit-condition</a></li>
  <li><strong>许可证</strong>: MIT License</li>
</ul>

<p>跟 <code class="language-plaintext highlighter-rouge">bit-ui</code> 的窗口系统配合起来用效果最好，红点挂在窗口内的 FairyGUI 节点上，窗口关闭时节点自动跟着销毁解绑。</p>]]></content><author><name>bitgong</name></author><category term="开源项目" /><category term="Cocos Creator" /><category term="FairyGUI" /><category term="TypeScript" /><category term="开源" /><category term="UI框架" /><summary type="html"><![CDATA[Cocos Creator 条件显示系统，用装饰器注册条件类型，自动计算红点、解锁提示的显示状态，定时批量更新省去手动刷新逐个节点。]]></summary></entry><entry><title type="html">bit-ecs：稀疏集合 + 密集数组撑起来的高性能 ECS</title><link href="https://me.bitgong.cn/posts/bit-ecs%E5%AE%9E%E4%BD%93%E7%BB%84%E4%BB%B6%E7%B3%BB%E7%BB%9F/" rel="alternate" type="text/html" title="bit-ecs：稀疏集合 + 密集数组撑起来的高性能 ECS" /><published>2026-08-15T02:00:00+00:00</published><updated>2026-08-15T02:00:00+00:00</updated><id>https://me.bitgong.cn/posts/bit-ecs%E5%AE%9E%E4%BD%93%E7%BB%84%E4%BB%B6%E7%B3%BB%E7%BB%9F</id><content type="html" xml:base="https://me.bitgong.cn/posts/bit-ecs%E5%AE%9E%E4%BD%93%E7%BB%84%E4%BB%B6%E7%B3%BB%E7%BB%9F/"><![CDATA[<h1 id="bit-ecs稀疏集合--密集数组撑起来的高性能-ecs">bit-ecs：稀疏集合 + 密集数组撑起来的高性能 ECS</h1>

<h2 id="它解决什么问题">它解决什么问题</h2>

<p>弹幕游戏、大型 RTS、需要同屏处理成百上千个单位的游戏，最容易被面向对象那一套写法拖垃：继承层级一深，一个”会飞的会打的会加血的怪”到底该继承哪个基类就能吵半天，而且对象越多，遍历判断类型的开销越大，GC 也跟着遭罪。</p>

<p>ECS（实体-组件-系统）架构把”是什么”（组件，纯数据）和”做什么”（系统，纯逻辑）拆开，实体只是个数字 ID，靠组件的组合决定它的行为。bit-ecs 在这个基础上，用稀疏集合加密集数组的数据布局，让频繁增删实体和组件这件事本身也做到高性能。</p>

<h2 id="安装">安装</h2>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>npm <span class="nb">install</span> @gongxh/bit-ecs
</code></pre></div></div>

<p>零依赖，独立使用。</p>

<h2 id="核心概念">核心概念</h2>

<ul>
  <li><strong>实体（Entity）</strong>——就是个数字 ID，本身不携带任何数据</li>
  <li><strong>组件（Component）</strong>——纯数据结构，比如位置、血量、速度</li>
  <li><strong>系统（System）</strong>——逻辑代码，处理”同时拥有某几种组件”的所有实体</li>
  <li><strong>世界（World）</strong>——管理实体、组件、系统的容器</li>
</ul>

<h2 id="核心用法">核心用法</h2>

<h3 id="定义组件">定义组件</h3>

<p>组件必须实现 <code class="language-plaintext highlighter-rouge">reset()</code>，因为组件是从对象池里回收复用的，不重置会带着上一个实体的脏数据：</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">Component</span><span class="p">,</span> <span class="nx">_ecsdecorator</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">@gongxh/bit-ecs</span><span class="dl">'</span><span class="p">;</span>
<span class="kd">const</span> <span class="p">{</span> <span class="nx">ecsclass</span><span class="p">,</span> <span class="nx">ecsprop</span> <span class="p">}</span> <span class="o">=</span> <span class="nx">_ecsdecorator</span><span class="p">;</span>

<span class="p">@</span><span class="nd">ecsclass</span><span class="p">(</span><span class="dl">'</span><span class="s1">Position</span><span class="dl">'</span><span class="p">)</span>
<span class="k">export</span> <span class="kd">class</span> <span class="nx">PositionComponent</span> <span class="kd">extends</span> <span class="nx">Component</span> <span class="p">{</span>
    <span class="p">@</span><span class="nd">ecsprop</span><span class="p">({</span> <span class="na">type</span><span class="p">:</span> <span class="dl">'</span><span class="s1">float</span><span class="dl">'</span> <span class="p">})</span> <span class="nx">x</span><span class="p">:</span> <span class="kr">number</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
    <span class="p">@</span><span class="nd">ecsprop</span><span class="p">({</span> <span class="na">type</span><span class="p">:</span> <span class="dl">'</span><span class="s1">float</span><span class="dl">'</span> <span class="p">})</span> <span class="nx">y</span><span class="p">:</span> <span class="kr">number</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>

    <span class="nx">reset</span><span class="p">():</span> <span class="k">void</span> <span class="p">{</span>
        <span class="k">this</span><span class="p">.</span><span class="nx">x</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
        <span class="k">this</span><span class="p">.</span><span class="nx">y</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<h3 id="定义系统">定义系统</h3>

<p>系统在 <code class="language-plaintext highlighter-rouge">onInit()</code> 里配置要查询哪些实体，<code class="language-plaintext highlighter-rouge">update(dt)</code> 里写每帧逻辑：</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">System</span><span class="p">,</span> <span class="nx">_ecsdecorator</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">@gongxh/bit-ecs</span><span class="dl">'</span><span class="p">;</span>
<span class="kd">const</span> <span class="p">{</span> <span class="nx">ecsystem</span> <span class="p">}</span> <span class="o">=</span> <span class="nx">_ecsdecorator</span><span class="p">;</span>

<span class="p">@</span><span class="nd">ecsystem</span><span class="p">(</span><span class="dl">'</span><span class="s1">Move</span><span class="dl">'</span><span class="p">)</span>
<span class="k">export</span> <span class="kd">class</span> <span class="nx">MoveSystem</span> <span class="kd">extends</span> <span class="nx">System</span> <span class="p">{</span>
    <span class="k">protected</span> <span class="nx">onInit</span><span class="p">():</span> <span class="k">void</span> <span class="p">{</span>
        <span class="k">this</span><span class="p">.</span><span class="nx">matcher</span><span class="p">.</span><span class="nx">allOf</span><span class="p">(</span><span class="nx">PositionComponent</span><span class="p">,</span> <span class="nx">VelocityComponent</span><span class="p">);</span>
    <span class="p">}</span>

    <span class="nx">update</span><span class="p">(</span><span class="nx">dt</span><span class="p">:</span> <span class="kr">number</span><span class="p">):</span> <span class="k">void</span> <span class="p">{</span>
        <span class="k">this</span><span class="p">.</span><span class="nx">query</span><span class="p">.</span><span class="nx">iterate2</span><span class="p">(</span><span class="nx">PositionComponent</span><span class="p">,</span> <span class="nx">VelocityComponent</span><span class="p">,</span> <span class="p">(</span><span class="nx">entity</span><span class="p">,</span> <span class="nx">pos</span><span class="p">,</span> <span class="nx">vel</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
            <span class="nx">pos</span><span class="p">.</span><span class="nx">x</span> <span class="o">+=</span> <span class="nx">vel</span><span class="p">.</span><span class="nx">x</span> <span class="o">*</span> <span class="nx">dt</span><span class="p">;</span>
            <span class="nx">pos</span><span class="p">.</span><span class="nx">y</span> <span class="o">+=</span> <span class="nx">vel</span><span class="p">.</span><span class="nx">y</span> <span class="o">*</span> <span class="nx">dt</span><span class="p">;</span>
        <span class="p">});</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">allOf</code> 表示实体必须同时有这两个组件才会被这个系统处理，还有 <code class="language-plaintext highlighter-rouge">anyOf</code>（任一）、<code class="language-plaintext highlighter-rouge">excludeOf</code>（必须不含）、<code class="language-plaintext highlighter-rouge">optionalOf</code>（可选，查询时一并带出但不作为过滤条件）可以组合。</p>

<p><code class="language-plaintext highlighter-rouge">iterate2</code> 这类按数量区分的迭代器（1~4 个组件）是零 GC 的，比通用的 <code class="language-plaintext highlighter-rouge">iterate(...types)</code>（支持最多 8 个组件，几乎无 GC）性能更好，热路径（每帧都跑的系统）优先用固定数量版本。</p>

<h3 id="创建世界跑起来">创建世界，跑起来</h3>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">World</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">@gongxh/bit-ecs</span><span class="dl">'</span><span class="p">;</span>

<span class="kd">const</span> <span class="nx">world</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">World</span><span class="p">(</span><span class="dl">'</span><span class="s1">battle</span><span class="dl">'</span><span class="p">,</span> <span class="mi">1024</span><span class="p">);</span> <span class="c1">// 最大实体数建议 2 的指数</span>
<span class="nx">world</span><span class="p">.</span><span class="nx">addSystem</span><span class="p">(</span><span class="k">new</span> <span class="nx">MoveSystem</span><span class="p">());</span>
<span class="nx">world</span><span class="p">.</span><span class="nx">initialize</span><span class="p">();</span> <span class="c1">// 必须调用一次</span>

<span class="kd">const</span> <span class="nx">entity</span> <span class="o">=</span> <span class="nx">world</span><span class="p">.</span><span class="nx">createEmptyEntity</span><span class="p">();</span>
<span class="nx">world</span><span class="p">.</span><span class="nx">addComponent</span><span class="p">(</span><span class="nx">entity</span><span class="p">,</span> <span class="nx">PositionComponent</span><span class="p">);</span>
<span class="nx">world</span><span class="p">.</span><span class="nx">addComponent</span><span class="p">(</span><span class="nx">entity</span><span class="p">,</span> <span class="nx">VelocityComponent</span><span class="p">);</span>

<span class="c1">// 每帧调用</span>
<span class="nx">world</span><span class="p">.</span><span class="nx">update</span><span class="p">(</span><span class="nx">dt</span><span class="p">);</span>
</code></pre></div></div>

<p>也可以用 <code class="language-plaintext highlighter-rouge">SystemGroup</code> 把系统分组，配置 <code class="language-plaintext highlighter-rouge">frameInterval</code> 让某些不需要每帧都跑的系统降频，比如 AI 决策系统每 3 帧跑一次：</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">aiGroup</span> <span class="o">=</span> <span class="nx">world</span><span class="p">.</span><span class="nx">SystemGroup</span><span class="p">(</span><span class="dl">'</span><span class="s1">AI</span><span class="dl">'</span><span class="p">,</span> <span class="mi">3</span><span class="p">);</span>
</code></pre></div></div>

<h2 id="命令缓冲为什么删除组件不是立刻生效">命令缓冲：为什么删除组件不是立刻生效</h2>

<p><code class="language-plaintext highlighter-rouge">removeEntity</code>、<code class="language-plaintext highlighter-rouge">addComponent</code>、<code class="language-plaintext highlighter-rouge">removeComponent</code> 都不是调用后立刻改变数据结构，而是延迟到下一帧 <code class="language-plaintext highlighter-rouge">update()</code> 前统一处理。这是命令缓冲模式，为的是避免系统正在遍历实体的过程中，数据结构被中途修改导致遍历错乱或者漏处理。</p>

<p>代价是：你在同一帧内调用 <code class="language-plaintext highlighter-rouge">removeComponent</code> 之后，马上再 <code class="language-plaintext highlighter-rouge">getComponent</code> 去读，读到的还是删除前的数据——它还没真正被移除。这个延迟生效的特性容易让人以为是 bug，实际是设计使然。</p>

<h2 id="性能特点">性能特点</h2>

<p>稀疏集合加密集数组这套布局，对”频繁加实体、删实体、加组件、删组件”这类场景做了偏向优化，系统遍历性能也不错，代价是内存占用会比纯稠密数组方案高一些——用空间换的是增删的时间。查询器本身会做条件复用，同样的查询条件多次调用不会重复计算，用了掩码做匹配，实体一多的时候也不会线性扫描全部组件类型。</p>

<h2 id="避坑提醒">避坑提醒</h2>

<ul>
  <li>组件的 <code class="language-plaintext highlighter-rouge">reset()</code> 一定要把所有字段都清干净，漏一个字段在对象池复用时就是隐藏的脏数据 bug，而且这种 bug 通常要跑很多局才会暴露，很难复现。</li>
  <li>增删操作是延迟生效的，同一帧内”删除再查询”拿到的是旧数据，不要依赖同帧内立即生效的假设写逻辑。</li>
  <li>世界的 <code class="language-plaintext highlighter-rouge">maxEntityCount</code> 建议设成 2 的指数（比如 512、1024），跟内部数据结构的扩容策略更契合。</li>
</ul>

<h2 id="项目信息">项目信息</h2>

<ul>
  <li><strong>GitHub</strong>: <a href="https://github.com/gongxh0901/bit-framework/tree/main/bit-ecs">https://github.com/gongxh0901/bit-framework/tree/main/bit-ecs</a></li>
  <li><strong>npm</strong>: <a href="https://www.npmjs.com/package/@gongxh/bit-ecs">@gongxh/bit-ecs</a></li>
  <li><strong>可视化编辑器</strong>: <a href="https://store.cocos.com/app/detail/7311">Cocos Store - kunpoec</a>（付费，基于 Creator 3.8.6 开发）</li>
  <li><strong>许可证</strong>: MIT License</li>
</ul>

<p>如果你的项目更偏向”节点少、逻辑相对简单”的中小型游戏，不需要 ECS 这么重的架构，可以看看更轻量的 <code class="language-plaintext highlighter-rouge">bit-ec</code>。</p>]]></content><author><name>bitgong</name></author><category term="开源项目" /><category term="Cocos Creator" /><category term="ECS" /><category term="TypeScript" /><category term="开源" /><category term="游戏架构" /><summary type="html"><![CDATA[高性能 ECS 框架，稀疏集合加密集数组实现零 GC 查询迭代，适合大量实体频繁增删的场景，配套可视化编辑器可视化配置组件。]]></summary></entry><entry><title type="html">bit-ec：不想上重型 ECS？试试这个 Cocos 专用轻量 EC</title><link href="https://me.bitgong.cn/posts/bit-ec%E8%BD%BB%E9%87%8F%E5%AE%9E%E4%BD%93%E7%BB%84%E4%BB%B6/" rel="alternate" type="text/html" title="bit-ec：不想上重型 ECS？试试这个 Cocos 专用轻量 EC" /><published>2026-08-14T02:00:00+00:00</published><updated>2026-08-14T02:00:00+00:00</updated><id>https://me.bitgong.cn/posts/bit-ec%E8%BD%BB%E9%87%8F%E5%AE%9E%E4%BD%93%E7%BB%84%E4%BB%B6</id><content type="html" xml:base="https://me.bitgong.cn/posts/bit-ec%E8%BD%BB%E9%87%8F%E5%AE%9E%E4%BD%93%E7%BB%84%E4%BB%B6/"><![CDATA[<h1 id="bit-ec不想上重型-ecs试试这个-cocos-专用轻量-ec">bit-ec：不想上重型 ECS？试试这个 Cocos 专用轻量 EC</h1>

<h2 id="它解决什么问题">它解决什么问题</h2>

<p><code class="language-plaintext highlighter-rouge">bit-ecs</code> 那种稀疏集合加密集数组的重型 ECS，是为了应付大批量实体高频增删设计的，但不是每个项目都有这个量级的需求。中小型项目里更常见的诉求其实是”数据和逻辑分开写，组件之间的更新顺序能精确控制，最好还能配个可视化编辑器直接拖属性”，上一套完整 ECS 有点杀鸡用牛刀。</p>

<p><code class="language-plaintext highlighter-rouge">bit-ec</code> 就是为这类场景做的：一个专为 Cocos Creator 优化的轻量实体组件框架，组件区分”数据组件”和”逻辑组件”，更新顺序你说了算，还支持多个世界（比如同时存在的多个战斗场景）互不干扰。</p>

<h2 id="安装">安装</h2>

<p><code class="language-plaintext highlighter-rouge">bit-event</code> 是 peer 依赖：</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>npm <span class="nb">install</span> @gongxh/bit-ec @gongxh/bit-event
</code></pre></div></div>

<h2 id="核心用法">核心用法</h2>

<h3 id="定义组件">定义组件</h3>

<p>组件继承 <code class="language-plaintext highlighter-rouge">Component</code>，用装饰器注册，属性装饰器支持的类型比较全，包括 Cocos 常用类型（<code class="language-plaintext highlighter-rouge">spriteframe</code>、<code class="language-plaintext highlighter-rouge">prefab</code>、<code class="language-plaintext highlighter-rouge">vec3</code>、<code class="language-plaintext highlighter-rouge">color</code> 等），方便配套编辑器直接可视化配置：</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">Component</span><span class="p">,</span> <span class="nx">_ecdecorator</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">@gongxh/bit-ec</span><span class="dl">'</span><span class="p">;</span>
<span class="kd">const</span> <span class="p">{</span> <span class="nx">ecclass</span><span class="p">,</span> <span class="nx">ecprop</span> <span class="p">}</span> <span class="o">=</span> <span class="nx">_ecdecorator</span><span class="p">;</span>

<span class="kr">enum</span> <span class="nx">ComponentType</span> <span class="p">{</span>
    <span class="nx">Health</span><span class="p">,</span>
    <span class="nx">Movable</span><span class="p">,</span>
<span class="p">}</span>

<span class="p">@</span><span class="nd">ecdecorator</span><span class="p">.</span><span class="nx">ecclass</span><span class="p">(</span><span class="dl">'</span><span class="s1">Health</span><span class="dl">'</span><span class="p">,</span> <span class="nx">ComponentType</span><span class="p">.</span><span class="nx">Health</span><span class="p">)</span>
<span class="kd">class</span> <span class="nx">HealthComponent</span> <span class="kd">extends</span> <span class="nx">Component</span> <span class="p">{</span>
    <span class="p">@</span><span class="nd">ecprop</span><span class="p">({</span> <span class="na">type</span><span class="p">:</span> <span class="dl">'</span><span class="s1">int</span><span class="dl">'</span> <span class="p">})</span> <span class="nx">maxHp</span><span class="p">:</span> <span class="kr">number</span> <span class="o">=</span> <span class="mi">100</span><span class="p">;</span>
    <span class="p">@</span><span class="nd">ecprop</span><span class="p">({</span> <span class="na">type</span><span class="p">:</span> <span class="dl">'</span><span class="s1">int</span><span class="dl">'</span> <span class="p">})</span> <span class="nx">curHp</span><span class="p">:</span> <span class="kr">number</span> <span class="o">=</span> <span class="mi">100</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<h3 id="组件生命周期">组件生命周期</h3>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nx">MoveComponent</span> <span class="kd">extends</span> <span class="nx">Component</span> <span class="p">{</span>
    <span class="nx">onAdd</span><span class="p">():</span> <span class="k">void</span> <span class="p">{</span>
        <span class="c1">// 组件被添加到实体的那一刻</span>
    <span class="p">}</span>

    <span class="nx">onEnter</span><span class="p">():</span> <span class="k">void</span> <span class="p">{</span>
        <span class="c1">// 实体正式进入场景，这时候可以安全地拿同实体的其他组件</span>
        <span class="kd">const</span> <span class="nx">health</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">getComponent</span><span class="o">&lt;</span><span class="nx">HealthComponent</span><span class="o">&gt;</span><span class="p">(</span><span class="nx">ComponentType</span><span class="p">.</span><span class="nx">Health</span><span class="p">);</span>
    <span class="p">}</span>

    <span class="nx">update</span><span class="p">(</span><span class="nx">dt</span><span class="p">:</span> <span class="kr">number</span><span class="p">):</span> <span class="k">void</span> <span class="p">{</span>
        <span class="c1">// 需要先把 needUpdate 设为 true 才会被调用</span>
    <span class="p">}</span>

    <span class="nx">onRemove</span><span class="p">():</span> <span class="k">void</span> <span class="p">{</span>
        <span class="c1">// 组件从实体移除</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">onAdd</code> 和 <code class="language-plaintext highlighter-rouge">onEnter</code> 分开，是因为组件刚被添加时，同实体上的其他组件可能还没就位，这时候去 <code class="language-plaintext highlighter-rouge">getComponent</code> 拿别的组件容易拿到 <code class="language-plaintext highlighter-rouge">undefined</code>。等到 <code class="language-plaintext highlighter-rouge">onEnter</code> 触发，实体上该有的组件都已经齐了，这时候拿组件互相引用才靠得住。</p>

<h3 id="创建世界和实体">创建世界和实体</h3>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">ECManager</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">@gongxh/bit-ec</span><span class="dl">'</span><span class="p">;</span>

<span class="nx">ECManager</span><span class="p">.</span><span class="nx">init</span><span class="p">();</span> <span class="c1">// 全局只调用一次</span>

<span class="c1">// componentUpdateOrderList 决定了各组件类型 update 的执行先后顺序</span>
<span class="kd">const</span> <span class="nx">world</span> <span class="o">=</span> <span class="nx">ECManager</span><span class="p">.</span><span class="nx">createECWorld</span><span class="p">(</span><span class="dl">'</span><span class="s1">battle-1</span><span class="dl">'</span><span class="p">,</span> <span class="nx">battleNode</span><span class="p">,</span> <span class="p">[</span>
    <span class="nx">ComponentType</span><span class="p">.</span><span class="nx">Movable</span><span class="p">,</span>
    <span class="nx">ComponentType</span><span class="p">.</span><span class="nx">Health</span><span class="p">,</span>
<span class="p">],</span> <span class="mi">300</span><span class="p">,</span> <span class="mi">10</span><span class="p">);</span>

<span class="kd">const</span> <span class="nx">entity</span> <span class="o">=</span> <span class="nx">ECManager</span><span class="p">.</span><span class="nx">createEntity</span><span class="p">(</span><span class="dl">'</span><span class="s1">battle-1</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">Monster_01</span><span class="dl">'</span><span class="p">);</span>

<span class="c1">// 每帧</span>
<span class="nx">world</span><span class="p">.</span><span class="nx">update</span><span class="p">(</span><span class="nx">dt</span><span class="p">);</span>
</code></pre></div></div>

<p>多个 <code class="language-plaintext highlighter-rouge">createECWorld</code> 可以同时存在，比如 PVP 场景里两个互相独立的小战场，各自的实体、组件互不干扰，一个世界 <code class="language-plaintext highlighter-rouge">update</code> 出问题不会影响另一个。</p>

<h2 id="组件更新顺序为什么要精确控制">组件更新顺序为什么要精确控制</h2>

<p>游戏逻辑里经常有依赖顺序的更新：移动组件先算完新位置，碰撞组件再基于新位置判断是否命中，血量组件最后根据命中结果扣血。如果更新顺序是随意的（比如按组件添加顺序或者哈希顺序），同样的代码在不同时候跑出来的行为可能不一致，调试起来很痛苦。</p>

<p><code class="language-plaintext highlighter-rouge">createECWorld</code> 的 <code class="language-plaintext highlighter-rouge">componentUpdateOrderList</code> 参数就是让你显式声明这个顺序，框架严格按这个顺序跑每种组件类型的 <code class="language-plaintext highlighter-rouge">update</code>，行为可预测。</p>

<h2 id="避坑提醒">避坑提醒</h2>

<ul>
  <li>别在 <code class="language-plaintext highlighter-rouge">onAdd()</code> 里调用 <code class="language-plaintext highlighter-rouge">getComponent()</code> 去拿同实体的其他组件，大概率拿不到，要拿组件互相引用的逻辑放到 <code class="language-plaintext highlighter-rouge">onEnter()</code> 里。</li>
  <li><code class="language-plaintext highlighter-rouge">update(dt)</code> 默认不会被调用，组件需要显式把 <code class="language-plaintext highlighter-rouge">needUpdate</code> 设为 <code class="language-plaintext highlighter-rouge">true</code>，纯数据组件不需要每帧更新的话别设，省一点遍历开销。</li>
  <li>多世界场景下，<code class="language-plaintext highlighter-rouge">createEntity</code> 第一个参数是世界名，同一个实体配置在不同世界里创建出来的是完全独立的两份实例，互相不共享状态。</li>
</ul>

<h2 id="项目信息">项目信息</h2>

<ul>
  <li><strong>GitHub</strong>: <a href="https://github.com/gongxh0901/bit-framework/tree/main/bit-ec">https://github.com/gongxh0901/bit-framework/tree/main/bit-ec</a></li>
  <li><strong>npm</strong>: <a href="https://www.npmjs.com/package/@gongxh/bit-ec">@gongxh/bit-ec</a></li>
  <li><strong>可视化编辑器</strong>: <a href="https://store.cocos.com/app/detail/7311">Cocos Store - kunpocc-ec</a>（付费）</li>
  <li><strong>许可证</strong>: MIT License</li>
</ul>

<p>组件之间跨系统通信、或者要通知 UI 层刷新，配合 <code class="language-plaintext highlighter-rouge">bit-event</code> 一起用最顺手，这也是它唯一的 peer 依赖。</p>]]></content><author><name>bitgong</name></author><category term="开源项目" /><category term="Cocos Creator" /><category term="EC" /><category term="TypeScript" /><category term="开源" /><category term="游戏架构" /><summary type="html"><![CDATA[面向 Cocos Creator 的轻量实体组件框架，支持组件更新顺序控制和多世界隔离，适合中小型项目在不引入重型 ECS 的情况下做数据与逻辑分离。]]></summary></entry><entry><title type="html">bit-event：一个零依赖的全局事件系统，模块解耦全靠它</title><link href="https://me.bitgong.cn/posts/bit-event%E4%BA%8B%E4%BB%B6%E7%B3%BB%E7%BB%9F/" rel="alternate" type="text/html" title="bit-event：一个零依赖的全局事件系统，模块解耦全靠它" /><published>2026-08-13T02:00:00+00:00</published><updated>2026-08-13T02:00:00+00:00</updated><id>https://me.bitgong.cn/posts/bit-event%E4%BA%8B%E4%BB%B6%E7%B3%BB%E7%BB%9F</id><content type="html" xml:base="https://me.bitgong.cn/posts/bit-event%E4%BA%8B%E4%BB%B6%E7%B3%BB%E7%BB%9F/"><![CDATA[<h1 id="bit-event一个零依赖的全局事件系统模块解耦全靠它">bit-event：一个零依赖的全局事件系统，模块解耦全靠它</h1>

<h2 id="它解决什么问题">它解决什么问题</h2>

<p>模块之间互相调用，最开始图省事直接 <code class="language-plaintext highlighter-rouge">import</code> 对方然后调方法，写着写着就变成了一张互相纠缠的依赖网——UI 模块 import 了战斗模块，战斗模块又 import 了 UI 模块，改一个地方不知道会牵动多远。事件系统是解这个问题最常见的办法：谁也不认识谁，只通过事件名义广播，听的人自己决定接不接。</p>

<p><code class="language-plaintext highlighter-rouge">bit-event</code> 是 bit-framework 里最基础的一个模块，零依赖，纯 TypeScript 实现，<code class="language-plaintext highlighter-rouge">bit-ec</code> 就是拿它做组件间通信的。</p>

<h2 id="安装">安装</h2>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>npm <span class="nb">install</span> @gongxh/bit-event
</code></pre></div></div>

<h2 id="核心用法">核心用法</h2>

<h3 id="全局事件">全局事件</h3>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">GlobalEvent</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">@gongxh/bit-event</span><span class="dl">'</span><span class="p">;</span>

<span class="c1">// 监听</span>
<span class="kd">const</span> <span class="nx">id</span> <span class="o">=</span> <span class="nx">GlobalEvent</span><span class="p">.</span><span class="nx">add</span><span class="p">(</span><span class="dl">'</span><span class="s1">mail:new</span><span class="dl">'</span><span class="p">,</span> <span class="p">(</span><span class="nx">mailId</span><span class="p">:</span> <span class="kr">number</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">收到新邮件</span><span class="dl">'</span><span class="p">,</span> <span class="nx">mailId</span><span class="p">);</span>
<span class="p">},</span> <span class="k">this</span><span class="p">);</span>

<span class="c1">// 发送</span>
<span class="nx">GlobalEvent</span><span class="p">.</span><span class="nx">send</span><span class="p">(</span><span class="dl">'</span><span class="s1">mail:new</span><span class="dl">'</span><span class="p">,</span> <span class="mi">1001</span><span class="p">);</span>

<span class="c1">// 只听一次，触发后自动移除，不用手动清理</span>
<span class="nx">GlobalEvent</span><span class="p">.</span><span class="nx">addOnce</span><span class="p">(</span><span class="dl">'</span><span class="s1">game:firstLogin</span><span class="dl">'</span><span class="p">,</span> <span class="nx">onFirstLogin</span><span class="p">);</span>
</code></pre></div></div>

<h3 id="移除监听四种方式挑一种">移除监听，四种方式挑一种</h3>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">GlobalEvent</span><span class="p">.</span><span class="nx">remove</span><span class="p">(</span><span class="nx">id</span><span class="p">);</span>                          <span class="c1">// 按具体的监听 ID</span>
<span class="nx">GlobalEvent</span><span class="p">.</span><span class="nx">removeByName</span><span class="p">(</span><span class="dl">'</span><span class="s1">mail:new</span><span class="dl">'</span><span class="p">);</span>             <span class="c1">// 这个事件名的所有监听都移除</span>
<span class="nx">GlobalEvent</span><span class="p">.</span><span class="nx">removeByTarget</span><span class="p">(</span><span class="k">this</span><span class="p">);</span>                 <span class="c1">// 这个 target 挂的所有监听都移除</span>
<span class="nx">GlobalEvent</span><span class="p">.</span><span class="nx">removeByNameAndTarget</span><span class="p">(</span><span class="dl">'</span><span class="s1">mail:new</span><span class="dl">'</span><span class="p">,</span> <span class="k">this</span><span class="p">);</span> <span class="c1">// 精确匹配名字+target</span>
</code></pre></div></div>

<p>实际项目里用得最多的是 <code class="language-plaintext highlighter-rouge">removeByTarget</code>——组件销毁的时候，一句话把它注册过的所有监听清空，不用一个个记 ID：</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">onDestroy</span><span class="p">():</span> <span class="k">void</span> <span class="p">{</span>
    <span class="nx">GlobalEvent</span><span class="p">.</span><span class="nx">removeByTarget</span><span class="p">(</span><span class="k">this</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<h3 id="需要隔离的场景自己-new-一个">需要隔离的场景，自己 new 一个</h3>

<p>如果某个子系统的事件不想跟全局事件混在一起（比如一个可以整体重置的小游戏内小游戏），创建独立实例，API 跟全局事件完全一样：</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">EventManager</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">@gongxh/bit-event</span><span class="dl">'</span><span class="p">;</span>

<span class="kd">class</span> <span class="nx">MiniGameContext</span> <span class="p">{</span>
    <span class="nx">event</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">EventManager</span><span class="p">();</span>
<span class="p">}</span>

<span class="c1">// 退出小游戏时一次清空，不影响全局事件系统</span>
<span class="nx">miniGameContext</span><span class="p">.</span><span class="nx">event</span><span class="p">.</span><span class="nx">clearAll</span><span class="p">();</span>
</code></pre></div></div>

<h2 id="最佳实践">最佳实践</h2>

<p>事件名建议用常量集中管理，别在业务代码里到处写字符串字面量，拼错一个字母编译期是发现不了的，只有运行时才会发现监听没触发。</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// events.ts 集中定义</span>
<span class="k">export</span> <span class="kd">const</span> <span class="nx">GameEvents</span> <span class="o">=</span> <span class="p">{</span>
    <span class="na">MailNew</span><span class="p">:</span> <span class="dl">'</span><span class="s1">mail:new</span><span class="dl">'</span><span class="p">,</span>
    <span class="na">FirstLogin</span><span class="p">:</span> <span class="dl">'</span><span class="s1">game:firstLogin</span><span class="dl">'</span><span class="p">,</span>
<span class="p">}</span> <span class="k">as</span> <span class="kd">const</span><span class="p">;</span>
</code></pre></div></div>

<p>另外要小心事件循环：事件 A 的回调里发了事件 B，事件 B 的回调里又发了事件 A，这种链路在代码量小的时候看不出来，模块一多很容易绕出一个死循环，排查起来要一路顺着监听关系摸，比较费时间。</p>

<h2 id="避坑提醒">避坑提醒</h2>

<ul>
  <li>忘记在对象销毁时移除监听，是这类事件系统最常见的内存泄漏来源——已经销毁的对象的回调函数还挂在事件表里，事件一发送就会尝试调用一个”死对象”上的方法。养成 <code class="language-plaintext highlighter-rouge">onDestroy</code> 里 <code class="language-plaintext highlighter-rouge">removeByTarget(this)</code> 的习惯基本能规避。</li>
  <li><code class="language-plaintext highlighter-rouge">send</code> 和 <code class="language-plaintext highlighter-rouge">sendToTarget</code> 的区别别搞混：<code class="language-plaintext highlighter-rouge">send</code> 是广播给所有监听这个名字的人，<code class="language-plaintext highlighter-rouge">sendToTarget</code> 只发给指定的那个 target，其他监听同名事件的人收不到。</li>
  <li>需要能整体清空、跟全局事件互不干扰的场景，直接用独立的 <code class="language-plaintext highlighter-rouge">EventManager</code> 实例，不要把这类临时性、局部性的事件也塞进全局事件里，退出的时候不好统一清理。</li>
</ul>

<h2 id="项目信息">项目信息</h2>

<ul>
  <li><strong>GitHub</strong>: <a href="https://github.com/gongxh0901/bit-framework/tree/main/bit-event">https://github.com/gongxh0901/bit-framework/tree/main/bit-event</a></li>
  <li><strong>npm</strong>: <a href="https://www.npmjs.com/package/@gongxh/bit-event">@gongxh/bit-event</a></li>
  <li><strong>许可证</strong>: MIT License</li>
</ul>

<p><code class="language-plaintext highlighter-rouge">bit-ec</code> 用它做组件间通信，是目前唯一把它当 peer 依赖的模块，但它本身完全独立，任何 TypeScript 项目都能直接拿去用，不限定 Cocos Creator。</p>]]></content><author><name>bitgong</name></author><category term="开源项目" /><category term="Cocos Creator" /><category term="TypeScript" /><category term="开源" /><category term="事件系统" /><summary type="html"><![CDATA[零依赖的轻量事件系统，支持按 ID、名称、目标对象批量移除监听，提供全局单例和独立实例两种用法，适合模块间解耦通信。]]></summary></entry></feed>