如果你在前端项目里用过 npx create-react-app my-app,或者用 uvx ruff check . 跑过 Python 的代码检查,大概已经习惯了那种“不用先安装、拿起来就用”的路子。但换到 .NET 这边,过去想跑一个社区命令行工具是另一番滋味:先 dotnet tool install --global dotnet-format,用完了想起来清理还得 dotnet tool uninstall。版本冲突、全局污染、CI 脚本里多一长串步骤……这些事老 .NET 开发者多少都经历过。
所以当我看到 .NET 10 把 dnx 作为一等公民带进 SDK 时,第一反应是:这个生态终于把欠下的工具链体验债还上了。dnx 的目标非常直白——让 .NET 命令行工具像 npx 和 uvx 那样,即用即走,按需解析,跑完拉倒。这篇文章我会从它的设计动机、实际操作到背后的机制取舍,完整拆一遍,顺便聊聊哪些坑是你在项目里真正要留意的。
1. 从 npx 和 uvx 说起:为什么“即用即走”是好体验
1.1 npx:把“临时安装”藏在背后
npx 是随 Node.js 一起分发的工具执行器,我第一次意识到它的威力是在一个没有 node_modules 的新项目里。当时要跑项目里配置的 ESLint,按老思路得先 npm install,然后写 ./node_modules/.bin/eslint。而 npx 的做法是:你直接 npx eslint src/,它自己去找本地装了没有,没装就临时下载一份,跑完放进缓存,下次再用。
这个机制带来的体验变化是巨大的。它把“工具安装”这个心智负担从用户那里抽走了,用户只需要知道“我要跑 eslint”,而不用关心“eslint 现在在哪个环境里、版本对不对、全局有没有”。对前端生态来说,npx 是创造性的简化,以至于后来很多脚手架工具都默认接在 npx 后面:npx create-react-app、npx storybook init 都是这个路子。
1.2 uvx:给 Python 工具一个隔离的临时家
Python 生态过去在工具分发上比 Node 更痛苦,因为 pip install 会把包装进全局 Python 环境,跑一遍就留下一堆依赖。后来出现 pipx,专门给命令行工具做隔离环境;而 uvx 作为 uv 项目的一部分,把这个隔离体验做得更轻。
用 uvx ruff check . 跑一遍代码检查,实际发生的事情是:uvx 在缓存里建一个临时虚拟环境,把 ruff 连同依赖装进去,执行后环境删除,只留下缓存。和 npx 的区别在于,Python 工具生态对依赖隔离的敏感度更高——两个工具如果依赖同一个库的冲突版本,全局环境根本没法共存。uvx 从根上避开了这个问题。这也是 npx/uvx 这类工具最值钱的那层保障:执行环境是临时构建的,互不干扰。
1.3 三者对照与核心体验
我把这种工具执行器的共性总结成了三句话:不污染当前环境,不需要手动安装,不需要记版本号。至于 npx、uvx 和这次登场的 dnx,放在同一张表里看会更清楚:
| 维度 | npx | uvx | dnx(.NET 10) |
|---|---|---|---|
| 所属生态 | Node.js | Python(uv 项目) | .NET SDK |
| 底层包来源 | npm registry | PyPI | NuGet |
| 执行方式 | 本地已有则直接跑,否则临时下载 | 临时虚拟环境隔离执行 | 本地工具有则跑,否则临时解析下载 |
| 版本指定 | npx pkg@version | uvx pkg==version | dnx pkg/version(预计支持) |
| 缓存策略 | npm 缓存目录 | uv 全局缓存 | NuGet 全局包缓存 |
