2026-01-23T08:11:08Z
https://github.com/bolasblack/BlogPosts/
c4605's blog
c4605
bolasblack@gmail.com
https://github.com/bolasblack
我写了一个 MCP 转 Skill 的 Skill
2026-01-23T00:00:00Z
2026-01-23T08:11:08Z
https://github.com/bolasblack/BlogPosts/blob/master/2026-01-23-mcp-skill-generator
<p>最近写了个 skill,功能是把 MCP server 转成 Claude Code skill。听起来有点绕,但其实挺实用的。</p><h2 id="为什么要转?">为什么要转?</h2><p>MCP 本身没什么问题,很好,至少给了一个统一的,可发现的交互协议,而且这个协议的接受度还这么广,太难得了,Restful 虽然很多服务商都支持,但是这个东西的约束太少,太灵活,实话说很多时候“可发现”这个事情很多大厂(尤其是不少中国的大厂)做的简直和屎一样;而 GraphQL, gRPC 之类的,接受度又都很有限,非常碎片化。有 MCP 至少这方面的问题解决很多。</p><p>但是它还是有点问题。</p><h3 id="上下文窗口吃的太严重了">上下文窗口吃的太严重了</h3><p>MCP 的工作方式是把所有 tool 的定义一股脑丢进上下文里。如果你接了好几个 MCP server,每个 server 又有一堆 tools,那上下文窗口里光是 tool 定义就占了一大坨,举个例子,我刚看了一下,GitHub mcp 占了我 10k token (5%) 的上下文。</p><p>Claude Code skill 有个叫 progressive disclosure(渐进式披露)的机制:AI 先看到一个简短的描述,只有在需要用到这个 skill 的时候才会加载完整的内容。这样上下文窗口就不会被一堆可能根本用不到的 tool 定义给塞满了。</p><p>虽然目前这个 skill 没办法把有些需要 OAuth 的 mcp 服务给变成 skill (严格来说,要做还是能做的,就是麻烦了点,我正在考虑后面有空的话就加上……),但是至少能少占一点是一点……</p><h3 id="ai-调用-tool-效率太低了">AI 调用 tool 效率太低了</h3><p>这是让我最想做这个转换的原因。</p><p>传统的方式是让 AI 一个一个地调用 tool:先调用 A,拿到结果,再调用 B,再调用 C……每次调用都要走一遍"AI 决策 → 发起调用 → 拿到结果 → AI 再决策"的循环。</p><p>问题是这个过程很不可靠。AI 可能会漏掉某个步骤,可能会在中间突然疯了,而且每次调用的结果都要塞进上下文里,token 消耗蹭蹭往上涨,甚至可能会因为返回的内容过长而失败,然后 AI 还得想办法多次分页请求内容。</p><p>更好的方式是让 AI 写一段脚本,一次性完成所有调用(或者对响应结果进行预处理)。代码的控制流比 AI 的"思考链"要可靠得多,而且 Anthropic 官方也在研究这个方向——他们发了一篇叫 <a href="https://www.anthropic.com/engineering/code-execution-with-mcp">Code execution with MCP</a> 的文章,数据显示 token 消耗从 15 万降到 2 千,减少了 98.7%。</p><p>我的这个 skill 生成的结果里就包含了一个 <code>api.mjs</code>,AI 可以直接 import 进来写脚本用:</p><pre><code class="language-javascript">import { callTool } from '/<mcp-skill-path>/scripts/api.mjs';
// 并行搜索多个仓库的 issues
const repos = ['facebook/react', 'vuejs/vue', 'sveltejs/svelte'];
const results = await Promise.all(
repos.map(repo => callTool('search_issues', {
repo,
query: 'memory leak'
}))
);
// 汇总结果,只返回需要的字段
const summary = results.flatMap((r, i) =>
r.content[0].text.items?.slice(0, 3).map(issue => ({
repo: repos[i],
title: issue.title,
url: issue.html_url
})) ?? []
);
console.log(JSON.stringify(summary, null, 2));
</code></pre><p>如果用传统的 tool calling 方式,AI 得调用 3 次 <code>search_issues</code>,每次调用的完整结果都要塞进上下文,然后 AI 再"思考"一遍怎么汇总。用脚本的话,一次执行完,返回给 AI 的就是处理好的精简结果。</p><h3 id="版本锁定">版本锁定</h3><p>MCP server 通常是 <code>npx @some/mcp-server</code> 这样运行的,每次都拉最新版。这意味着 server 的维护者随时可以推一个恶意更新,而你完全不知道。</p><p>转成 skill 的时候可以锁定版本号,至少能防一手供应链攻击。</p><h2 id="简单示例">简单示例</h2><p>用起来大概是这样:</p><pre><code>> /mcp-skill-generator 帮我转一下 @anthropic/mcp-server-filesystem
> /mcp-skill-generator 帮我转一下 https://docs.devin.ai/work-with-devin/deepwiki-mcp
</code></pre><p>然后它会问你要存到哪里(项目目录还是个人目录),接着自动提取 schema、生成文件。最后得到的目录结构是:</p><pre><code>mcp-filesystem/
├── SKILL.md
├── config.toml
├── tools/
│ ├── read_file.md
│ ├── write_file.md
│ └── ...
└── scripts/
├── mcp-caller.mjs
├── ...
└── api.mjs # AI 可以用这个写脚本
</code></pre><h2 id="最后">最后</h2><p>代码在 <a href="https://github.com/user/claude-skills">我的 claude-skills 仓库</a> 里,有兴趣的可以看看。</p><p>说实话这个 skill 本身就是用 Claude Code 写的,写的过程中我深刻体会到了"让 AI 写代码"比"让 AI 调用 tool"靠谱多少——至少代码跑不通的时候能看到报错信息,而不是 AI 默默地走偏了你还不知道。</p>
c4605
bolasblack@gmail.com
https://github.com/bolasblack
shadow-cljs-vite-plugin v0.0.6: 修复 HMR 和 ES Module 兼容性问题
2026-01-22T00:00:00Z
2026-01-22T13:45:44Z
https://github.com/bolasblack/BlogPosts/blob/master/2026-01-22-shadow-cljs-vite-plugin-v0.0.6
<p><a href="https://github.com/bolasblack/shadow-cljs-vite-plugin">shadow-cljs-vite-plugin</a> 首次发布两周后,v0.0.6 带来了一些重要的修复,让开发体验更加顺滑。</p><h2 id="问题一:es-module-导入">问题一:ES Module 导入</h2><p>首次发布时,ClojureScript 代码在 Vite 的 ESM 环境中存在一些导入问题,在部署到 Cloudflare Workers 时尤为明显。</p><p>这个问题需要上游配合修复,我们向 shadow-cljs 提交了一个 <a href="https://github.com/thheller/shadow-cljs/pull/1249">PR #1249</a>,已在 v3.3.5 合并。感谢 <a href="https://github.com/thheller">@thheller</a> 一起解决这个问题!</p><h2 id="问题二:hmr-时-"namespace-already-declared"-错误">问题二:HMR 时 "Namespace already declared" 错误</h2><p>这个问题比较恼人。在热更新时,Vite 会重新执行模块代码,但全局状态(比如 <code>goog.loadedModules_</code>)会跨 HMR 更新持久化。Google Closure Library 假设模块只会被加载一次,遇到重复注册就会抛错。</p><p>解决方案是让 <code>goog.provide</code> 和 <code>goog.module</code> 变成幂等操作:</p><pre><code class="language-javascript">goog.provide = function(name) {
return goog.isProvided_(name) ? undefined : goog.constructNamespace_.call(this, name);
};
goog.module = Object.assign(function(name) {
if (name in goog.loadedModules_) {
goog.moduleLoaderState_.moduleName = name;
return;
}
return origModule.call(this, name);
}, origModule);
</code></pre><h2 id="更好的-hmr-批处理">更好的 HMR 批处理</h2><p>之前如果你快速保存多个文件(或者编辑器保存时自动格式化),每次变更都会触发一次 HMR 更新。现在插件会使用防抖机制将多个文件变更合并为单次更新。</p><p>另外,修改 <code>shadow-cljs.edn</code> 后 Vite 会自动重启,不用再手动重启了。</p><h2 id="tailwind-css-支持">Tailwind CSS 支持</h2><p>插件和 Tailwind CSS 配合良好,我已经在<a href="https://blog.c4605.com/">我的博客</a>前端使用这个组合,没有任何问题。</p><h2 id="试试看">试试看</h2><pre><code class="language-bash">npm install shadow-cljs-vite-plugin
</code></pre><p>插件已经稳定,我正在生产环境使用。欢迎在 <a href="https://github.com/bolasblack/shadow-cljs-vite-plugin">GitHub</a> 上反馈和贡献。</p>
c4605
bolasblack@gmail.com
https://github.com/bolasblack
bitcoin-wallet-connector: 可能是目前最好用的比特币钱包适配器
2026-01-06T00:00:00Z
2026-01-11T18:56:04Z
https://github.com/bolasblack/BlogPosts/blob/master/2026-01-06-bitcoin-wallet-connector-introduction
<p>标题党一下 🤪</p><p><a href="https://blog.c4605.com/goto/articles/2026-01-03-bitcoin-wallet-ecosystem-is-a-mess.md">上一篇文章</a>我吐槽了比特币钱包生态有多混乱:WBIPs 标准没人实现、sats-connect 的兼容性、钱包各有各的 API...</p><p>这篇文章介绍下我写的库:<a href="https://github.com/bolasblack/bitcoin-wallet-connector">bitcoin-wallet-connector</a>。</p><p>先来试试 <strong><a href="https://bitcoin-wallet-connector.netlify.app/iframe.html?id=components-bitcoinconnectionprovider--default&viewMode=story">在线 Demo</a></strong> ,或者 <strong><a href="https://bitcoin-wallet-connector.netlify.app/">Storybook</a></strong></p><h2 id="我来讲讲这个库关注什么">我来讲讲这个库关注什么</h2><h3 id="把那些莫名其妙的差异抹平">把那些莫名其妙的差异抹平</h3><p><code>bitcoin-wallet-connector</code> 提供了一套统一的 API,让你用同样的代码接入所有支持的钱包:</p><pre><code class="language-typescript">import {
BitcoinWalletConnector,
UnisatWalletAdapterFactory,
XverseWalletAdapterFactory,
LeatherWalletAdapterFactory,
} from "bitcoin-wallet-connector";
// 注册你想支持的钱包
const connector = new BitcoinWalletConnector([
UnisatWalletAdapterFactory(),
XverseWalletAdapterFactory(),
LeatherWalletAdapterFactory(),
]);
// 订阅可用钱包变化
// 注意:钱包扩展注入 API 的时机是不确定的(有些在 DOMContentLoaded,
// 有些在 load 事件后),所以推荐使用 subscribe 而不是 get
connector.subscribeAvailableAdapters((availableAdapters) => {
console.log(
"可用钱包:",
availableAdapters.map(([id]) => id)
);
// => ['unisat', 'xverse', ...]
});
// 连接钱包 - 所有钱包用同一套 API
const [adapterId, adapter] = availableAdapters[0];
await connector.connect(adapterId, adapter);
// 获取地址、签名、发送交易 - 统一接口
const addresses = await adapter.getAddresses();
const result = await adapter.signMessage(
addresses[0].address,
"Hello Bitcoin!"
);
</code></pre><p>就这样,你只需要写一遍代码,就能支持所有钱包。</p><p>目前支持的钱包:</p><table><thead><tr><th>钱包</th><th>Adapter</th><th>额外依赖</th></tr></thead><tbody><tr><td><a href="https://unisat.io/">UniSat</a></td><td><code>UnisatWalletAdapterFactory</code></td><td>-</td></tr><tr><td><a href="https://www.xverse.app/">Xverse</a></td><td><code>XverseWalletAdapterFactory</code></td><td><code>sats-connect</code></td></tr><tr><td><a href="https://www.okx.com/web3">OKX</a></td><td><code>OkxWalletAdapterFactory</code></td><td>-</td></tr><tr><td><a href="https://leather.io/">Leather</a></td><td><code>LeatherWalletAdapterFactory</code></td><td><code>@leather.io/rpc</code></td></tr><tr><td><a href="https://web3.bitget.com/">Bitget</a></td><td><code>BitgetWalletAdapterFactory</code></td><td>-</td></tr><tr><td><a href="https://wallet.magiceden.io/">Magic Eden</a></td><td><code>MagicEdenWalletAdapterFactory</code></td><td><code>sats-connect</code></td></tr></tbody></table><p>所有 adapter 都实现了相同的接口:</p><pre><code class="language-typescript">interface WalletAdapter {
// 连接/断开
connect(): Promise<void>;
disconnect(): Promise<void>;
// 获取地址
getAddresses(): Promise<WalletAdapterAddress[]>;
// 消息签名
signMessage(address: string, message: string): Promise<SignMessageResult>;
// 发送 BTC
sendBitcoin(
fromAddress: string,
receiverAddress: string,
satoshiAmount: bigint
): Promise<{ txid: string }>;
// PSBT 签名
signAndFinalizePsbt(
psbtHex: string,
signIndices: [address: string, signIndex: number][]
): Promise<{ signedPsbtHex: string }>;
// 监听地址变化
onAddressesChanged(callback): { unsubscribe: () => void };
}
</code></pre><p>无论用户选择哪个钱包,你的业务代码都是一样的。</p><h4 id="关于-sendrunes/sendinscriptions/sendbrc20">关于 sendRunes/sendInscriptions/sendBRC20</h4><p>目前这个库只支持 <code>signMessage</code>, <code>sendBitcoin</code>, 和 <code>signPsbt</code>,暂时不支持 <code>sendRunes</code>, <code>sendInscriptions</code>, <code>sendBRC20</code>。</p><p>因为这些都涉及到更复杂的依赖(比如需要一个 Ordinals Indexer, 还需要一个 BRC20 Indexer 等等)。这些会让一个 <code>Connector</code> 变的过于复杂。</p><p>在我看来,这应该是一个 <code>Transaction Builder</code> 的职责,由 <code>Transaction Builder</code> 负责构建交易,然后将交易交给 <code>Connector</code> 来签名和发送。</p><h3 id="安全性很重要">安全性很重要</h3><p>在设计这个库的时候,我把<strong>依赖安全</strong>放在了很高的优先级。原因很简单:钱包库直接接触用户的资产,任何安全漏洞都可能造成真金白银的损失。</p><h4 id="peer-dependencies">Peer Dependencies</h4><p>我把一些重要的依赖声明为 peer dependencies,而不是打包进库里:</p><pre><code class="language-bash">pnpm add bitcoin-wallet-connector @scure/base @scure/btc-signer
</code></pre><p>这意味着:</p><ul><li>你可以直接控制这些依赖的版本</li><li>如果某个依赖爆出安全漏洞,你可以<strong>立即升级</strong>,不用等这个库发新版本</li><li>也不会出现最终的 bundle 里被打包了两个版本的 <code>@scure/btc-signer</code> 的尴尬场景</li></ul><h4 id="可选依赖:按需安装">可选依赖:按需安装</h4><p>钱包 SDK(如 <code>sats-connect</code>、<code>@leather.io/rpc</code>)是<strong>可选</strong>的 peer dependencies:</p><pre><code class="language-bash"># 只支持 UniSat 和 OKX?不需要安装任何额外依赖
# 需要支持 Xverse?
pnpm add sats-connect
# 需要支持 Leather?
pnpm add @leather.io/rpc
</code></pre><p>你只安装你需要的,减少你被恶意包脚本攻击的风险。</p><h4 id="动态导入:延迟加载">动态导入:延迟加载</h4><p>这是另外一个重要的安全设计:<strong>钱包 SDK 通过 <code>dynamic import()</code> 延迟加载</strong>。</p><pre><code class="language-typescript">// 内部实现示意
const availability = createAvailability({
getPrecondition: () => window.unisat ?? null,
initializer: async () => {
// 只有用户真正要连接这个钱包时,才会加载对应的实现
const { UnisatWalletAdapterImpl } = await import(
"./UnisatWalletAdapter.impl"
);
return new UnisatWalletAdapterImpl();
},
});
</code></pre><p>假设 <code>sats-connect</code> 这个包被供应链攻击了(这在 npm 生态并不罕见)。如果你的用户只使用 UniSat 钱包,<strong>恶意代码不会被加载和执行</strong>,因为 <code>sats-connect</code> 只有在用户点击"连接 Xverse"时才会被 import。</p><p>这一条应该能降低用户被供应链攻击的风险。</p><h2 id="框架集成">框架集成</h2><p>目前提供了 React 集成,开箱即用的 Context Provider:</p><pre><code class="language-tsx">import {
BitcoinConnectionProvider,
useBitcoinConnectionContext,
} from "bitcoin-wallet-connector/react";
import {
UnisatWalletAdapterFactory,
XverseWalletAdapterFactory,
} from "bitcoin-wallet-connector/adapters";
const adapterFactories = [
UnisatWalletAdapterFactory(),
XverseWalletAdapterFactory(),
];
function App() {
return (
<BitcoinConnectionProvider
adapterFactories={adapterFactories}
onWalletConnected={(session) => console.log("Connected:", session)}
onWalletDisconnected={() => console.log("Disconnected")}
>
<WalletUI />
</BitcoinConnectionProvider>
);
}
function WalletUI() {
const { walletSession, availableAdapters, connect, disconnect } =
useBitcoinConnectionContext();
if (walletSession) {
return (
<div>
<p>已连接: {walletSession.adapterId}</p>
<button onClick={() => disconnect()}>断开连接</button>
</div>
);
}
return (
<div>
{availableAdapters.map(([adapterId, adapter]) => (
<button key={adapterId} onClick={() => connect(adapterId, adapter)}>
连接 {adapterId}
</button>
))}
</div>
);
}
</code></pre><p>核心的 <code>BitcoinWalletConnector</code> 是框架无关的,如果你在用 Vue、Svelte、Solid 或其他框架,封装成对应的 hooks/composables 应该不难,当然也欢迎贡献!</p><h2 id="欢迎使用和贡献">欢迎使用和贡献</h2><h3 id="快速开始">快速开始</h3><pre><code class="language-bash">pnpm add bitcoin-wallet-connector @scure/base @scure/btc-signer
# 按需安装钱包 SDK
pnpm add sats-connect # Xverse / Magic Eden
pnpm add @leather.io/rpc # Leather
</code></pre><p>或者 5 分钟跑通 Demo:</p><ol start="1"><li>Clone 仓库</li><li><code>pnpm install</code></li><li><code>pnpm storybook</code></li><li>打开 <a href="http://localhost:6006">http://localhost:6006</a></li></ol><p>你可以在 Storybook 里测试各种钱包的连接、签名等功能。</p><h3 id="贡献">贡献</h3><p>这是一个开源项目,非常欢迎社区贡献!</p><p>如果你想添加新钱包的支持,有一个小小的期望:<strong>尽量不要依赖钱包官方的 SDK</strong>。</p><p>为什么?因为很多钱包的 API 其实就是挂在 <code>window</code> 对象上的,直接调用就行,没必要引入一个额外的 SDK。比如 UniSat、OKX、Bitget 的 adapter 都是零外部依赖的。</p><p>引入 SDK 意味着多一个潜在的供应链攻击入口、用户需要多安装一个包、可能引入不必要的 bundle size。当然,如果某个钱包确实只能通过 SDK 接入(比如 Xverse 的 <code>sats-connect</code>),那也没问题,我们可以把它作为可选的 peer dependency。</p><p>详细的贡献指南请看 <a href="https://github.com/bolasblack/bitcoin-wallet-connector/blob/master/CONTRIBUTING.md">CONTRIBUTING.md</a>。</p><hr /><p>项目完全开源(MIT),欢迎 Star 和贡献!</p><ul><li>GitHub: <a href="https://github.com/bolasblack/bitcoin-wallet-connector">https://github.com/bolasblack/bitcoin-wallet-connector</a></li><li>Demo: <a href="https://bitcoin-wallet-connector.netlify.app/">https://bitcoin-wallet-connector.netlify.app/</a></li><li>npm: <a href="https://www.npmjs.com/package/bitcoin-wallet-connector">https://www.npmjs.com/package/bitcoin-wallet-connector</a></li></ul>
c4605
bolasblack@gmail.com
https://github.com/bolasblack
比特币钱包生态有多土狗:从开始到放弃
2026-01-03T00:00:00Z
2026-01-05T10:05:48Z
https://github.com/bolasblack/BlogPosts/blob/master/2026-01-03-bitcoin-wallet-ecosystem-is-a-mess
<p>如果你正在开发一个比特币 dApp,你可能已经发现了,比特币社区的生态实在是太土狗了。</p><p>最简单的例子就是比特币钱包,大家似乎热衷于给其他人制定标准,但大家似乎又不热衷于实现这个标准。</p><h2 id=""标准"?什么标准?">"标准"?什么标准?</h2><p>让例子更详细一点:</p><h3 id="wbips:制定标准的人自己都不遵守">WBIPs:制定标准的人自己都不遵守</h3><p>Leather 和 Xverse 搞了一个 <a href="https://wbips.netlify.app/">WBIPs</a> ,说是要制定一个统一的比特币钱包 API 标准,但:</p><p><strong>signPsbt 的 finalize 参数</strong></p><p><a href="https://github.com/wbips/wbips-docs/blob/a7862572785686da8c0097125366086fea3c70d6/pages/request_api/signPsbt.md"><code>signPsbt</code></a> 标准里定义了 <code>finalize</code> 参数,但两个发起者都没实现:</p><ul><li>Leather 的<a href="https://github.com/leather-io/mono/blob/2570b8e89e1a7d67ff2ec469c42b321f5d956e92/packages/rpc/src/methods/bitcoin/sign-psbt.ts#L25-L35">代码</a></li><li>Xverse 的<a href="https://github.com/secretkeylabs/sats-connect-core/blob/cfbbdfde28a646f1b5114dad39ec76da85c3ffe9/src/request/types/btcMethods.ts#L178-L192">代码</a></li></ul><p><strong>钱包自动发现机制的实现完全不对啊老师</strong></p><p><a href="https://github.com/wbips/wbips-docs/blob/a7862572785686da8c0097125366086fea3c70d6/pages/wbips/WBIP004.md">WBIP004</a> 定义了钱包插件的自动发现机制,第一步是往 <code>wbip_providers</code> 里注入钱包 provider。</p><p>但实际上 Xverse 和 Leather 都是往 <code>window.btc_providers</code> 里注入的:</p><ul><li>Leather 的<a href="https://github.com/leather-io/mono/blob/2570b8e89e1a7d67ff2ec469c42b321f5d956e92/packages/provider/src/add-leather-to-providers.ts#L21-L52">代码</a></li><li>Xverse 的<a href="https://github.com/secretkeylabs/xverse-web-extension/blob/bdbd6a008fd3d8f04ea18eca0374fae67d90114c/src/inpage/index.ts#L53">代码</a></li></ul><p><strong>另外两个主流钱包根本不 care</strong></p><p>更不用说实际上占有大量流量的 <strong>UniSat</strong> 和 <strong>OKX Wallet</strong> 似乎对支持 WBIPs 没有丝毫兴趣。</p><h3 id="sats-connect:标准之上的标准">sats-connect:标准之上的标准</h3><p>然后在 WBIPs 之外,Xverse 还搞了一个 <a href="https://github.com/xverseapp/sats-connect">sats-connect</a> ,似乎想在 WBIPs 之外再扩展一些自己的 API ,实现一套标准之上的标准。</p><p>这不算差,尤其是它还兼容了 UniSat API 。Magic Eden 还号称 "兼容 sats-connect"。</p><p>"这听起来简直太棒了!sats-connect 简直就是版本答案!世界救主!",对吧?你肯定是这么想的。</p><p>不过完美世界的上空"还剩下两朵小小的乌云":</p><ol start="1"><li><strong>sats-connect 不支持 OKX Wallet</strong>(虽然 OKX Wallet 的 API 现在已经迁移到和 UniSat 兼容了,但 sats-connect 没办法自动发现它)</li><li><strong>Magic Eden 的自动发现跟的是另一个标准</strong>:<a href="https://github.com/ExodusMovement/bitcoin-wallet-standard"><code>ExodusMovement/bitcoin-wallet-standard</code></a>,而不是 WBIPs 标准。所以你没办法靠 sats-connect 来自动发现它。</li></ol><h3 id="等下,又来一个标准?">等下,又来一个标准?</h3><p>"等下卧槽,这个 <code>ExodusMovement/bitcoin-wallet-standard</code> 又是个什么标准?我 TM 从来没听说过啊!"</p><p>对的,在我尝试集成 Magic Eden 之前,我也没听说过,现在我听说了,你也听说了。</p><h3 id="更多-fun-facts">更多 Fun Facts</h3><p>噢,还有一个 "fun fact" ,如果你的用户同时安装了 Xverse 和 Magic Eden ,而且你没有处理好,那么你的用户在点击连接钱包时,可能会<strong>随机收到来自两个中其中一个钱包的确认请求</strong>,人生真是处处充满惊喜。</p><p>噢,再加一个 "fun fact" ,Magic Eden 虽然号称 "兼容 sats-connect",但实际上它兼容的是<strong>旧版本的 API</strong> 。</p><h2 id="来点示例代码">来点示例代码</h2><p>你可能会觉得那大不了我就自己在项目里都集成一遍呗。那让我们来看看为了实现一个简单的连接钱包功能,代码的差异:</p><pre><code class="language-typescript">// UniSat - 直接调用 window 对象
const unisatResponse = await window.unisat.requestAccounts();
// Leather - 又是另一套 API
const leatherResponse = await window.LeatherProvider.request("getAddresses");
// Xverse - 来自官方文档
import { request } from "sats-connect";
const xverseResponse = await request("getAddresses", null);
// Magic Eden - 来自官方文档
import { getAddress, AddressPurpose, BitcoinNetworkType } from "sats-connect";
await getAddress({
getProvider: getBtcProvider,
payload: {
purposes: [AddressPurpose.Ordinals, AddressPurpose.Payment],
message: "Address for receiving Ordinals and payments",
network: {
type: BitcoinNetworkType.Mainnet,
},
},
onFinish: (response) => {
console.log("onFinish response, ", response.addresses);
connectionStatus?.setAccounts(response.addresses as unknown as Account[]);
},
onCancel: () => {
alert("Request canceled");
},
});
</code></pre><p>四个钱包,四种写法。</p><p>如果你得在你的项目里集成 6 个钱包呢?恭喜你,你会需要把我上面提到的问题都过一遍,和我一起愉快地浪费掉一大把人生的时间。</p><h2 id="总结一下">总结一下</h2><p>"去他妈的傻逼比特币钱包生态,太他妈土狗了!",你/我心想。</p><h2 id="所以我写了一个库">所以我写了一个库</h2><p>所以我写了一个 <a href="https://github.com/bolasblack/bitcoin-wallet-connector">bitcoin-wallet-connector</a> ,可以让其他人少浪费一点时间在这些破事上。</p><p><strong>下一篇文章</strong>我会详细介绍这个库的使用方法和设计理念。如果你已经等不及了,可以先看看:</p><ul><li><strong><a href="https://bitcoin-wallet-connector.netlify.app/iframe.html?id=components-bitcoinconnectionprovider--default&viewMode=story">在线 Demo</a></strong></li><li><strong><a href="https://github.com/bolasblack/bitcoin-wallet-connector">GitHub 仓库</a></strong></li></ul>
c4605
bolasblack@gmail.com
https://github.com/bolasblack
我把我的配置共享仓库转移到了 LLM-oriented 模式
2026-01-02T00:00:00Z
2026-01-05T10:05:48Z
https://github.com/bolasblack/BlogPosts/blob/master/2026-01-02-moved-to-llm-oriented
<h2 id="啥是-"llm-oriented-的配置共享方案"">啥是 "LLM-oriented 的配置共享方案"</h2><p>简单来说,就是让配置共享项目提供 <a href="https://llmstxt.org/">llms.txt</a> ,使用方依此让 LLM 去下载/更新项目的配置文件,以保持配置的更新。而不是通过 npm registry 来分发配置。</p><h2 id="那为不用-npm-了?">那为不用 npm 了?</h2><p>传统的工具配置共享方式,无论是通过 <code>extends</code> 继承还是 preset 预设,都面临一个根本性的矛盾:<strong>便利性与灵活性的两难选择</strong>。</p><h3 id="继承模式的优点">继承模式的优点</h3><p>当你使用类似 <code>extends: "@company/eslint-config"</code> 的方式时,好处显而易见:</p><ul><li><strong>升级简单</strong>:只需 <code>npm update</code>,所有使用该配置的项目都能获得更新</li><li><strong>一致性保证</strong>:团队中所有项目使用相同的规则</li><li><strong>维护集中</strong>:配置的改进和 bug 修复只需要在一个地方进行</li></ul><h3 id="但现实很骨感">但现实很骨感</h3><p>问题出现在你需要做任何"非标准"操作的时候:</p><ol start="1"><li><strong>自定义需求</strong>:项目有特殊需求,需要覆盖某些规则</li><li><strong>依赖版本冲突</strong>:你需要升级 ESLint,但上游配置包还没有跟进(比如 ESLint 更新到 flat mode config 了,你需要用到最新版的 ESLint 了,但是我懒,我还没更新 🤪)</li><li><strong>规则分歧</strong>:上游更新了某个规则,但你的项目不适用(问题是你一开始可能不知道,等出问题了才发现"卧槽我被坑了")</li></ol><p>这时候,你有两个选择:</p><p><strong>选择 A:在继承基础上打补丁</strong></p><pre><code class="language-javascript">// eslint.config.js
export default [
...baseConfig,
{
rules: {
// 覆盖一个规则
"some-rule": "off",
// 再覆盖一个
"another-rule": ["error", { different: "options" }],
},
},
// 还要处理插件版本冲突...
];
</code></pre><p>补丁越打越多,最终你的"继承"变成了一层又一层的覆盖,反而比从头写还难维护。</p><p>ESLint 其实已经还好了,Prettier 和 lint-staged 这种没有 extends/merge 机制的就更麻烦了,我之前写了一个超级麻烦的 lint-staged 配置(看这里 <a href="https://github.com/bolasblack/js-metarepo/blob/0270b291651bcbc75a3ecd81d391f4ad836a814e/packages/toolconfs/lint-staged.config.js"><code>lint-staged.config.js</code></a> 和 <a href="https://github.com/bolasblack/js-metarepo/blob/0270b291651bcbc75a3ecd81d391f4ad836a814e/packages/toolconfs/lint-staged.helpers.js"><code>lint-staged.helpers.js</code></a>,看起来挺 decent 的(好吧至少我自己这么觉得),其实超级难用,还把一个非常简单的事情搞的极其复杂</p><p><strong>选择 B:Eject(弹出)</strong></p><p>很多脚手架工具提供了 <code>eject</code> 命令,把所有配置文件复制到你的项目里,让你获得完全控制权。</p><p>但 eject 之后:</p><ul><li>你失去了自动升级的能力,你得自己去更新,没办法享受上游已经测试过的插件之间的兼容性</li><li>当上游有重要更新时,你需要手动 diff 和合并</li></ul><p><strong>这就是两难困境</strong>:继承模式下你受制于上游,eject 之后你要承担维护成本。</p><h2 id="llm-是个好东西">LLM 是个好东西</h2><p>现在我们有了 LLM 了,时代变了</p><h3 id="新的工作流">新的工作流</h3><ol start="1"><li><strong>获取最新配置</strong>:LLM 直接从 llms.md/llms.txt 获取最新的推荐配置</li><li><strong>智能合并</strong>:LLM 理解你的项目上下文,把新配置合并到现有配置中</li><li><strong>保留自定义</strong>:你的自定义修改会被识别并保留</li><li><strong>按需更新</strong>:当你需要升级时,让 LLM 帮你处理合并冲突</li></ol><h3 id="为什么这比传统方式更好?">为什么这比传统方式更好?</h3><p><strong>所有配置都在你的代码库中</strong></p><p>没有隐藏的 <code>node_modules</code> 深处的配置文件,没有"这个规则是从哪里来的"的困惑。每一行配置都清清楚楚地在你的项目里。</p><p><strong>升级变得可控</strong></p><p>升级不再是一个"要么全要,要么不要"的决定。LLM 可以帮你:</p><ul><li>查看上游有什么变化</li><li>理解每个变化的影响</li><li>选择性地应用更新</li><li>自动处理合并冲突</li></ul><p><strong>自定义是一等公民</strong></p><p>你的自定义不再是"补丁",而是配置的一部分。LLM 在更新时会理解并尊重这些自定义。</p><p><strong>依赖版本自由</strong></p><p>你可以随时升级任何依赖,不需要等待上游配置包更新。遇到问题?还能让 LLM 帮你调整配置(他们读起配置来也变简单了)。</p><p><strong>上游的配置文件变的更简单了</strong></p><p>还是看我的 lint-staged 配置文件</p><p>以前的版本:</p><ul><li><a href="https://github.com/bolasblack/js-metarepo/blob/0270b291651bcbc75a3ecd81d391f4ad836a814e/packages/toolconfs/lint-staged.config.js"><code>lint-staged.config.js</code></a></li><li>为了方便下游能复用配置,还能自定义 lint-staged 的配置,不得不写的帮助函数 <a href="https://github.com/bolasblack/js-metarepo/blob/0270b291651bcbc75a3ecd81d391f4ad836a814e/packages/toolconfs/lint-staged.helpers.js"><code>lint-staged.helpers.js</code></a></li></ul><p>现在的版本:</p><ul><li><a href="https://github.com/bolasblack/js-metarepo/blob/25d4495adcba67bda9daf65b04a204acc01cb795/packages/toolconfs/lint-staged.config.js"><code>lint-staged.config.js</code></a> 算了,这个实在是太简单了,我直接 inline 了得了:<pre><code class="language-javascript">module.exports = {
"*.{ts,tsx,js,jsx}": ["prettier --write", "eslint"],
"*.{css,scss,sass,less,md,mdx}": ["prettier --write"],
};
</code></pre></li><li><code>lint-staged.helpers.js</code> 已经不存在了,因为没必要</li></ul><h2 id="你说了这么多,但这看起来不还是倒退吗?">你说了这么多,但这看起来不还是倒退吗?</h2><p>是,也不是。</p><ul><li><strong>是</strong>:配置文件确实在每个项目中独立存在,和最早的时候一样</li><li><strong>不是</strong>:因为维护成本发生了本质变化:<ul><li><strong>以前</strong>:人工去看 CHANGELOG 或者看和上游配置文件的 diff -> 搞明白啥变了 -> 手动修改 -> 测试验证</li><li><strong>现在</strong>:告诉 LLM "升级到最新推荐配置" -> 审查 LLM 的改动 -> 完成</li></ul></li></ul><p>维护的认知负担从"我需要理解所有这些配置"变成了"我需要审查 LLM 的建议"。这是和最早的时候不同的地方。</p><h2 id="总结(ai-版,我自己编不出来)">总结(AI 版,我自己编不出来)</h2><p>传统的配置共享是在"一刀切的便利"和"完全自主的复杂"之间做选择。LLM-oriented 的方式让我们可以两者兼得:</p><ul><li>保持配置的完全所有权和透明度</li><li>同时享受智能辅助的升级和维护</li></ul><p>这不是技术的倒退,而是利用新工具重新定义了最佳实践。</p>
c4605
bolasblack@gmail.com
https://github.com/bolasblack
我搞了一个 Elm 风格的 useEffectReducer hook
2025-11-12T00:00:00Z
2026-01-05T10:05:48Z
https://github.com/bolasblack/BlogPosts/blob/master/2025-11-12-elm-style-useeffectreducer-hook-for-react
<p><strong>GitHub:</strong> <a href="https://github.com/bolasblack/react-components/tree/develop/packages/useEffectReducer">bolasblack/react-components/tree/develop/packages/useEffectReducer</a></p><h2 id="什么是-"elm-风格"?">什么是 "Elm 风格"?</h2><p>Elm 有一个 <strong>Model-View-Update</strong> 模式,其中每次更新都会返回下一个 model 和要执行的命令:</p><pre><code class="language-elm">update : Msg -> Model -> ( Model, Cmd Msg )
</code></pre><p>这种设计将所有关于 <em>"事件 X 发生时会发生什么"</em> 的逻辑集中在 <strong>一个地方</strong> —— update 函数 —— 而不是将副作用分散在各种 <code>useEffect</code> 中。</p><p>它还保持了 reducer 的纯洁性,同时让 <strong>副作用何时发生、发生什么</strong> 变得明确,并可被运行时解释执行。</p><p>React 官方文档也呼应了这一理念:effect 应该是一个 <em>逃生舱</em>,而不是默认选择 —— 参见 <a href="https://react.dev/learn/you-might-not-need-an-effect">You Might Not Need an Effect</a>。</p><blockquote><p>如果你想了解这种建模方式如何让 UI 逻辑变得优雅且易于维护,可以看看 David Khourshid 的经典文章 <a href="https://dev.to/davidkpiano/no-disabling-a-button-is-not-app-logic-598i"><strong>"No, disabling a button is not app logic."</strong></a></p></blockquote><h2 id="为什么-又-造一个-elm-风格的-reducer?">为什么 <em>又</em> 造一个 Elm 风格的 reducer?</h2><p>因为没有完全满足我的需求的版本:</p><ul><li><p><strong>有些已经归档了。</strong> 例如,<a href="https://github.com/davidkpiano/useEffectReducer"><code>useEffectReducer</code></a> 和 <a href="https://github.com/soywod/react-use-bireducer"><code>react-use-bireducer</code></a> 现在都是只读状态。</p></li><li><p><strong>保持精简。</strong> 只需一个文件,几乎没有依赖 —— 随时可以复制、修改或删除。</p></li><li><p><strong>Effect 作为普通对象 + 独立的解释器。</strong> 返回可序列化的 effect descriptors,然后在一个专门的地方实现实际的 effect 逻辑。这样更容易测试 reducer(只需断言描述符),而无需真正执行副作用。</p></li><li><p><strong>降低门槛。</strong> 目标是让 Elm 风格的方法变得易于上手,而不需要深入了解 Elm 的 <code>Cmd Msg</code> 系统,也不需要学习像 <a href="https://xstate.js.org/">XState</a> 这样的完整状态机库。</p></li></ul><h2 id="来点代码">来点代码</h2><p>我们用 <code>useEffectReducer</code> 重新实现一下 David Khourshid 文章 <a href="https://dev.to/davidkpiano/no-disabling-a-button-is-not-app-logic-598i"><strong>"No, disabling a button is not app logic."</strong></a> 中的示例。</p><p>两种实现都可以在 GitHub 上找到:</p><ul><li><strong>useEffectReducer 版本:</strong> <a href="https://github.com/bolasblack/react-components/blob/eb47a2416e8cc95bb4fa7e6fbf776ac2432a2468/packages/useEffectReducer/src/useEffectReducer.stories.tsx#L121-L203">useEffectReducer.stories.tsx → L121–203</a></li><li><strong>useReducer 版本:</strong> <a href="https://github.com/bolasblack/react-components/blob/eb47a2416e8cc95bb4fa7e6fbf776ac2432a2468/packages/useEffectReducer/src/useEffectReducer.stories.tsx#L24-L119">useEffectReducer.stories.tsx → L24–119</a></li></ul><h2 id="其他人的工作">其他人的工作</h2><p>将 <strong>Elm 风格的 "状态 + 副作用" reducer</strong> 引入 React 的优秀现有实现:</p><ul><li><strong><a href="https://github.com/davidkpiano/useEffectReducer"><code>davidkpiano/useEffectReducer</code></a></strong> — 作者 <em>David Khourshid</em>;已归档</li><li><strong><a href="https://github.com/soywod/react-use-bireducer"><code>soywod/react-use-bireducer</code></a></strong> — 返回 <code>[state, effects]</code> 并通过独立的 effect reducer 处理副作用;已归档</li><li><strong><a href="https://github.com/ncthbrt/react-use-elmish"><code>ncthbrt/react-use-elmish</code></a></strong> — Elmish 风格的 hook,结合了 reducer 逻辑和异步辅助函数</li><li><strong><a href="https://github.com/redux-loop/redux-loop"><code>redux-loop</code></a></strong> — Redux 增强器,添加类似 Elm 的 effect 元组</li><li><strong><a href="https://github.com/dai-shi/use-reducer-async"><code>dai-shi/use-reducer-async</code></a></strong> — 扩展 <code>dispatch</code> 以支持异步 action</li><li><strong><a href="https://gist.github.com/sophiebits/145c47544430c82abd617c9cdebefee8"><code>useReducerWithEmitEffect</code></a></strong> — Sophie Alpert 的 gist,启发了本工作的大部分内容</li></ul><h2 id="好文章">好文章</h2><ul><li>Christian Ekrem - "<a href="https://cekrem.github.io/posts/chapter-2-take-2/">Chapter 2, Take 2: Why I Changed Course</a>"</li><li>David Khourshid – "<a href="https://dev.to/davidkpiano/redux-is-half-of-a-pattern-1-2-1hd7">Redux is half of a pattern (1/2)</a>"</li><li>David Khourshid – "<a href="https://medium.com/@DavidKPiano/there-are-so-many-fundamental-misunderstandings-about-xstate-and-state-machines-in-general-in-13aec57d2f85">There are so many fundamental misunderstandings about XState (and state machines in general)</a>"</li><li>David Khourshid – "<a href="https://dev.to/davidkpiano/no-disabling-a-button-is-not-app-logic-598i">No, disabling a button is not app logic.</a>"</li><li>React docs – "<a href="https://react.dev/learn/you-might-not-need-an-effect">You Might Not Need an Effect</a>"</li></ul>
c4605
bolasblack@gmail.com
https://github.com/bolasblack
TypeScript 如何真正 keyof 一个枚举
2020-05-05T00:00:00Z
2026-01-04T21:17:00Z
https://github.com/bolasblack/BlogPosts/blob/master/2020-05-05-TypeScript_如何真正_keyof_一个枚举
<p>有一个朋友问我怎么才能 <code>keyof</code> 一个 TypeScript 里的枚举,因为如果你直接 <code>keyof Enum</code> 的话,得到的结果是类似 <code>keyof number</code> ,而不是得到枚举的所有可能的 key 。</p><p>于是我简单写了一下跟他解释,顺手发上来:</p><p>在 TypeScript 里,你在声明一个枚举的时候,其实声明了两个类型,一个是枚举容器的类型(是一个对象),一个是枚举成员的类型(是数字/字符串或者其他类型的子类型)。</p><p>TypeScript 的 <code>keyof</code> 操作符预设后面跟着的是类型,所以我们 <code>keyof Enum</code> 时,其实相当于是在 <code>keyof (作为 number 的子类型的 Enum)</code> ,也就是相当于 <code>keyof number</code> ,所以会得到 <code>number</code> 的属性和方法名。</p><p>而 <code>typeof</code> 操作符预设后面跟的是一个值,所以当我们这么写 <code>keyof typeof Enum</code> 的时候,我们是把 <code>Enum</code> 作为一个值(也就是上文提到的“对象”),展开来以后就是 <code>keyof (作为类型的 Enum)</code> ,所以能够得到枚举的 key 。</p><p>相关资料:</p><ul><li><a href="https://github.com/microsoft/TypeScript/issues/14106">keyof Enum - microsoft/TypeScript#14106 - GitHub</a></li></ul>
c4605
bolasblack@gmail.com
https://github.com/bolasblack
尝试用 TypeScript 模拟一个带 Payload 的枚举
2020-05-01T00:00:00Z
2026-01-04T21:17:00Z
https://github.com/bolasblack/BlogPosts/blob/master/2020-05-01-尝试用_TypeScript_模拟一个带_Payload_的枚举
<p>这两天逛 GitHub 时突然又注意到了 <a href="https://github.com/origamitower/folktale">folktale</a> ,发现它现在带了一个 <a href="https://folktale.origamitower.com/api/v2.3.0/en/folktale.adt.union.union.union.html">adt/union</a> 。看起来 folktale 现在的 <code>Maybe</code>, <code>Result</code>, <code>Validation</code> 都是从 union 里派生出来的,使用方法和 Swift/Rust 里的枚举非常相似:</p><pre><code class="language-javascript">const Either = union("Either", {
Left: (value) => ({ value }),
Right: (value) => ({ value }),
});
console.log(
Either.Right(1).matchWith({
Left: ({ value }) => `left ${value}`,
Right: ({ value }) => `right ${value}`,
})
);
</code></pre><p>正好我想在 TypeScript 里模拟一个带 Payload 的枚举机制已经想了很久了,觉得这个 API 设计得不错,可惜的是没有 TypeScript 的类型定义,在各种抓耳挠腮之下写了一个不完美的版本(下面解释):</p><pre><code class="language-typescript">// 这是一个假的函数,只是做个占位而已
export function union<P extends BaseUnionPatterns>(
typeId: string,
patterns: P
): UnionTypes<P> {
return null as any;
}
export type UnionModelMatchPatterns<P extends BaseUnionPatterns, R> = {
[K in keyof P]: (patternFnReturn: ReturnType<P[K]>) => R;
};
export type UnionModelMatchPatternsWithAny<P extends BaseUnionPatterns, R> = {
[K in keyof P]?: (patternFnReturn: ReturnType<P[K]>) => R;
} & {
$$any: (patternFnReturn: ReturnType<P[keyof P]>) => R;
};
export type UnionInstance<P extends BaseUnionPatterns, Type extends keyof P> = {
equals: (unionInstance: UnionInstance<P, any>) => boolean;
matchWith: <R>(
branches:
| UnionModelMatchPatterns<P, R>
| UnionModelMatchPatternsWithAny<P, R>
) => R;
};
export interface UnionType<P extends BaseUnionPatterns, K extends keyof P> {
(...args: Parameters<P[K]>): UnionInstance<P, K>;
hasInstance: (model: UnionInstance<P, keyof P>) => boolean;
}
export type UnionTypes<P extends BaseUnionPatterns> = {
[K in keyof P]: UnionType<P, K>;
};
export interface BaseUnionPatterns {
[type: string]: (...args: any) => any;
}
</code></pre><p>事情并不如愿,总归还是没有办法完全实现我想要的效果,最终的成效是:</p><pre><code class="language-typescript">// 在这段代码里
// Maybe 变量的类型是对的
const Maybe = union("Maybe", {
// patterns 参数的类型约束是对的
Nothing: () => null,
Just: <T>(value: T) => ({ value }),
Other: <T>(arg1: T, rest: T[]) => [arg1].concat(rest),
});
// a 变量的类型是对的
const a = Maybe.Just(1);
// b 变量的类型被成功推断
const b = a.matchWith({
// branches 的类型约束是对的
Nothing: (arg) => "no value", // arg 的类型确实是 null
Just: (arg) => `value: ${arg.value}`, // arg 的类型是 { value: unknown } (问题一)
Other: (arg) => `values: ${arg.join(",")}`, // arg 的类型是 unknown[] (问题二)
});
a.matchWith({
Just: (arg) => `value: ${arg.value}`,
// TypeScript 里指定的 Symbol 没办法作为 key 的类型,所以只能用特殊 key $$any
// 来替代 folktale 里的 any Symbol (问题三)
$$any: (arg) => `values: ${arg}`,
});
console.log(b);
</code></pre><p>我们来一个个看这些问题:</p><ul><li>关于问题一,让人比较不爽,毕竟 <code>Maybe.Just</code> 的参数类型是出现过的,只是因为 <code>patterns</code> 参数里的 <code>Just</code> 函数是一个泛型函数,而我们在做类型转换的时候又没办法传递类型参数,所以没办法推断出 <code>{ value: T }</code> 里 <code>T</code> 的类型;</li><li>关于问题二,其实可以理解的,毕竟整个 <code>a</code> 变量的生命周期里从来没提到过 <code>Other</code> 的 Payload 类型;</li><li>关于问题三,和问题一一样,都是属于语言设施功能上的缺位,大概只能等 TypeScript 支持相关的特性。</li></ul><p>后来结合问题一问题二一想,其实这两个问题可以尝试一起解决,我们只要在声明变量 <code>a</code> 的时候给它传递一个更完整的类型信息就可以了,于是又一阵抓耳挠腮之后,终于写出来一个粗糙的方案:</p><pre><code class="language-typescript">export type UnionV<
T extends UnionTypes<any>,
WrappedValues extends T extends UnionTypes<infer P>
? { [K in keyof P]: ReturnType<P[K]> }
: never
> = UnionInstance<
{ [K in keyof T]: (...args: Parameters<T[K]>) => WrappedValues[K] },
keyof T
>;
const Maybe = union("Maybe", {
Nothing: () => null,
Just: <T>(value: T) => ({ value }),
Others: <T>(con: T, rest: T[]) => [con].concat(rest),
});
type MaybeNumber = UnionV<
typeof Maybe,
{
// 这里填进去 patterns 各个函数的返回值类型
Nothing: null;
Just: { value: number };
Others: number[];
}
>;
const a = Maybe.Just("a") as MaybeNumber;
// 这回 b 的类型被成功推断为 number | null
const b = a.matchWith({
Nothing: (arg) => arg,
Just: (arg) => arg.value, // arg 的类型为 { value: number }
Others: (items) => items.length, // arg 的类型为 number[]
});
console.log(b);
</code></pre><p>目前这套方案在 <code>patterns</code> 的参数都是具体的类型时,不需要 <code>UnionV</code> 就能够工作得很好;在出现泛型函数时,我只能想到通过 <code>UnionV</code> 来为 tsc 提供额外的类型信息。</p><p>那么,我们这算是大功告成了吗?并没有,因为整套方案是真的不够优雅,<code>UnionV</code> 的第二个类型参数实在是让人倒胃口。而且如果你仔细看的话,会发现我上面这段代码里 <code>Maybe.Just</code> 的参数是一个字符串,而 tsc 并没有报错。我知道是因为我用了转型,但是如果我这么写 <code>a: MaybeNumber =</code> ,tsc 会报错说 <code>Type '{ value: unknown; }' is not assignable to type '{ value: number; }'.</code> ,所以……这就是为什么这个方案不够优雅的原因之二了。</p><p>目前还想不出来更好的方案,只能再等等了,看看 TypeScript 接下来会不会提供这套方案需要的东西吧。</p>
c4605
bolasblack@gmail.com
https://github.com/bolasblack
无固有尺寸的 SVG 图片在各浏览器中的渲染尺寸问题
2020-04-09T00:00:00Z
2026-01-04T21:17:00Z
https://github.com/bolasblack/BlogPosts/blob/master/2020-04-09-无固有尺寸的_SVG_图片在各浏览器中的渲染尺寸问题
<h2 id="故事">故事</h2><p>公司同事在做图片预览功能时发现,很多 SVG 图片在 Firefox 下通过 <code>img</code> 标签加载成功后,<code>naturalWidth</code>/<code>naturalHeight</code> 是 0,和其他浏览器表现不同。</p><p>这个事情让我有点好奇,按照经验,我觉得 Firefox 一般是最跟随标准的,但是它又何其他浏览器的实现都不相同。所以查了一些资料做了一些测试。经过简单测试后发现,没有 固有尺寸<sup class="sidenote-ref" data-label="intrinsic-dimensions">1</sup> 的 SVG 在不同浏览器上使用 <code>img</code> 标签渲染时的尺寸是不同的。</p><p>我用几个浏览器跑了一遍 <a href="https://codepen.io/AmeliaBR/pen/gvEJWr/">CodePen 上的例子</a><sup class="sidenote-ref" data-label="github-#3510">2</sup></p><p>这个例子一共有四张图:</p><ol start="1"><li>有 <code>viewBox</code> 属性的正方图片</li><li>有 <code>viewBox</code> 属性的非正方图片</li><li>没有 <code>viewBox</code> 属性的非正方图片</li><li>有 <code>viewBox</code> 和 <code>width</code>, <code>height</code> 属性的非正方图片</li></ol><p>分别在 Firefox v74, Chrome v80, Safari v13 里跑了一下代码:</p><pre><code class="language-js">document.querySelectorAll("img").forEach((el) => {
console.log(
"width",
el.width,
"height",
el.height,
"naturalWidth",
el.naturalWidth,
"naturalHeight",
el.naturalHeight
);
});
</code></pre><p>结果如下:</p><ul><li>Firefox v74:<ul><li><code>img.naturalWidth</code>/<code>img.naturalHeight</code><ul><li>1, 2, 3 的这个属性都返回 0</li><li>4 返回 SVG 图片 <code>width</code>, <code>height</code> 属性的值</li></ul></li><li><code>img.width</code>/<code>img.height</code><ul><li>1, 2 的 <code>img.width</code> 都是 <code>100%</code> 的计算值,<code>img.height</code> 按比例缩放</li><li>3 返回 <code>300x150</code></li><li>4 返回 SVG 图片 <code>width</code>, <code>height</code> 属性的值</li></ul></li></ul></li><li>Chrome v80<ul><li><code>img.naturalWidth</code>/<code>img.naturalHeight</code><ul><li>1, 2 的 <code>naturalHeight</code> 都返回 <code>150</code> ,<code>naturalWidth</code> 按比例缩放</li><li>3 返回 <code>300x150</code></li><li>4 返回 SVG 图片 <code>width</code>, <code>height</code> 属性的值</li></ul></li><li><code>img.width</code>/<code>img.height</code> 和 Firefox 表现一致</li></ul></li><li>Safari v13<ul><li><code>img.naturalWidth</code>/<code>img.naturalHeight</code><ul><li>1, 2 的 <code>naturalWidth</code> 都返回 <code>100%</code> 的计算值,<code>naturalHeight</code> 按比例缩放</li><li>3 返回 <code>300x150</code></li><li>4 返回 SVG 图片 <code>width</code>, <code>height</code> 属性的值</li></ul></li><li><code>img.width</code>/<code>img.height</code> 和 Firefox 表现一致</li></ul></li></ul><p>然后我查了一下 <a href="https://html.spec.whatwg.org/commit-snapshots/a027acbded508ca06c67fbc9e550e22072681463/#dom-img-naturalwidth">2020-04-09 时的 HTML 的标准对 <code>naturalWidth</code> 的描述</a>:</p><blockquote><p>The IDL attributes <code>naturalWidth</code> and <code>naturalHeight</code> must return the density-corrected intrinsic width and height of the image, in CSS pixels, if the image has intrinsic dimensions and is available, or else 0.</p></blockquote><p>看起来好像是 Firefox 的实现比较贴近当前的 HTML 标准。</p><h2 id="那么如果我想体面地渲染一张随机的-svg-图片,我该怎么办呢?">那么如果我想体面地渲染一张随机的 SVG 图片,我该怎么办呢?</h2><p>目前我没有想到什么特别好的方案,可能会需要自己根据需求来确定方案</p><ul><li>如果可以获取到 SVG 文件的代码,而且可以执行 JavaScript ,那么我们可以通过判断 <code><svg></code> 标签有没有 <code>width</code>, <code>height</code> 来确认图片是否有固有尺寸,有的话就直接使用 <code>naturalWidth</code>/<code>naturalHeight</code> ,没有的话就使用一 <code>img.width</code>/<code>img.height</code> 计算出宽高比,然后使用我们预置的宽度/高度来渲染图片,另外一边等比缩放</li><li>如果没办法获取 SVG 文件的代码,或者没办法执行 JavaScript ,那么我们就只能给图片的外部套一个限制了宽高的容器了。如果图片有固有尺寸,那么图片会按照固有尺寸进行渲染(如果超出了我们限制的宽高它会等比缩放);如果没有固有尺寸,它就会自动缩放以适应我们限制的宽高</li></ul><h2 id="其他资料">其他资料</h2><ul><li><a href="https://thatemil.com/blog/2014/04/06/intrinsic-sizing-of-svg-in-responsive-web-design/">Intrinsic sizing of SVG in responsive web design - That Emil.</a></li></ul>
c4605
bolasblack@gmail.com
https://github.com/bolasblack
关于 React 和 Angular 的看法
2015-06-03T00:00:00Z
2026-01-04T21:17:00Z
https://github.com/bolasblack/BlogPosts/blob/master/2015-06-03-关于_React_和_Angular_的看法
<p>我最近也用了一段时间的 reactjs ,所以我想有还是稍微有点资格来谈论 angularjs 和 reactjs 的。</p><p>有一些文章会提到 angularjs 的两个很关键的问题:controller 拥有了自己的状态,和声明依赖比较容易出现循环依赖。</p><p>Facebook 确实提出了一个非常不错的解决方案,但这方案与 Virtual DOM 没有直接的关系(一些文章总是提这东西,我承认这是一个很重要的特性,但和解决 angularjs 的问题关系不大)。</p><p>这个方案是 <a href="http://facebook.github.io/flux/">Flux</a> :通过发布事件的方式来提醒模型更新状态,进而更新所有相关 DOM ,一个很巧妙的解决方案。而 Virtual DOM 只是这个解决方案里保证性能的一环而已。</p><p>而 data-binding ,实话说看到一些 reactjs 的拥趸一天到晚扯 data-binding 是邪恶的让我觉得很莫名其妙。data-binding 只是一种模式而已,reactjs 内置了数据的单向绑定难道这就不是 data-binding ?而且 reactjs 还带了一个扩展 <a href="http://facebook.github.io/react/docs/two-way-binding-helpers.html">ReactLink</a> 用来实现数据的双向绑定又是什么意思?难道在 Vitrual DOM 的 <code>onChange</code> 属性里传一个回调函数然后更新 <code>state</code> 或者发出事件更新模型就不是数据绑定了?那么其他框架实现双向绑定的方法 “监听 DOM 事件,更新 ViewModel” 也不能算是双向绑定咯?因为这其实和在 reactjs 里做的没有任何区别。</p><p>我的观点就是,reactjs 是一个不错的东西,Flux 解决了不少其他 MVVM 框架的问题,而 data-binding 绝对是一种进步,因为它大量的减少了开发者的代码量(早就有人 <a href="https://github.com/spoike/refluxjs#using-refluxconnect">实现</a> 了一个使用 flux 模式的双向绑定扩展了)。</p>
c4605
bolasblack@gmail.com
https://github.com/bolasblack