- 前端
- 跨平台
- UI组件
- 桌面应用
- 移动开发
【免费下载链接】dioxus
Fullstack app framework for web, desktop, and mobile.
在 Dioxus 这个号称"一套代码编译到 Web、桌面与移动端"的全栈框架中,同一份rsx!代码之所以能在不同平台自动选择不同的渲染后端,离不开一套位于编译期的"条件代码生成"基础设施——Config Macros(配置宏)。本文以仓库内 config-macros 的 README 为切入点,结合dioxus-config-macro、dioxus-config-macros两个 crate 的源码,以及dioxus主 crate 的 Feature 接线,完整拆解这套机制:它如何把 Cargo Feature 翻译成宏可见的编译条件,如何实现 Wasm 分包(wasm-split)时"左右两支 token"的二选一,又如何在路由与组件代码生成中被真实调用。读完本文,你将理解 Dioxus 平台无关架构中"编译期平台分派"这一关键环节的实现原理。
一、背景:为什么需要"配置宏"
README 开篇即给出定位:
These macros are used internally by codegen and are not intended for general use.
也就是说,dioxus-config-macros不是给应用开发者用的公共 API,而是供 Dioxus 内部代码生成器(codegen)使用的编译期开关。Dioxus 的核心设计是"一个代码库、多平台目标":rsx!、#[component]、#[derive(Routable)]这些宏在展开时,必须知道当前正在为哪个目标编译(Web?桌面?服务端渲染?Liveview?),从而生成不同的代码路径。
问题在于:普通macro_rules!无法直接读取 Cargo Feature。于是 Dioxus 采用了一个巧妙的方案——把 Feature 状态"导出"到宏能感知的载体上。README 第二句点明了这一机制:
Dioxus will export its feature flags into this crate, allowing downstream codegen to use them under the "dioxus" namespace.
即:dioxus主 crate 会把它的 Feature 传递到dioxus-config-macros(以及dioxus-config-macro),下游代码生成器通过dioxus::config_macros::xxx!这样的命名空间路径来消费这些开关。这个传递链路正是本文要拆解的核心。
二、仓库全景:两个"配置宏" crate 的分工
在仓库packages/目录下存在两个名字高度相似的 crate,它们承担不同职责:
| Crate 目录 | 类型 | 职责 |
|---|---|---|
| packages/config-macros | 普通macro_rules!宏 | 提供maybe_wasm_split!,依赖为零,仅一个wasm-splitfeature |
| packages/config-macro | 过程宏(proc_macro) | 提供server_only!、client!、web!等九个平台判断宏 |
前者(复数macros)的 Cargo.toml 显示其没有任何依赖,只有一个空 feature 标记wasm-split = [];后者(单数macro)在 src/lib.rs 中通过一个define_config_macro!辅助宏批量定义了全部平台宏。两者的组合,构成了 Dioxus 编译期平台分派的两套工具。
在dioxus主 crate 中,两者的暴露方式也不同:
- packages/dioxus/src/lib.rs 中无条件执行
pub use dioxus_config_macros as config_macros;,使maybe_wasm_split!始终以dioxus::config_macros::maybe_wasm_split!形式可用; - 而
dioxus-config-macro的过程宏在 packages/dioxus/src/lib.rs 中被#[cfg(feature = "launch")]门控后,以pub use dioxus_config_macro::*;进入prelude,因此应用代码里可以直接写server_only!、web!等名字。
三、maybe_wasm_split!:Wasm 分包时代的"左右分支"选择器
dioxus-config-macros的核心资产是maybe_wasm_split!。该宏的完整源码位于 packages/config-macros/src/lib.rs,它以两个同名定义 + 不同#[cfg]条件的方式实现分支选择:
/// Only on wasm with the wasm-split feature will we prefer the `maybe_wasm_split` variant that emits /// the "lefthand" tokens. Otherwise, we emit the non-wasm_split tokens #[doc(hidden)] #[cfg(all(feature = "wasm-split", target_arch = "wasm32"))] #[macro_export] macro_rules! maybe_wasm_split { ( if wasm_split { $left:tt } else { $right:tt } ) => { $left }; } #[doc(hidden)] #[cfg(any(not(feature = "wasm-split"), not(target_arch = "wasm32")))] #[macro_export] macro_rules! maybe_wasm_split { ( if wasm_split { $left:tt } else { $right:tt } ) => { $right }; }这段代码值得逐层解读:
- 调用方语法:调用时必须提供形如
if wasm_split { ... } else { ... }的两组 token($left:tt与$right:tt),这里tt(token tree)匹配意味着左右两侧可以是任意复杂度的代码块。 - 语义:当且仅当同时满足
feature = "wasm-split"且target_arch = "wasm32"时,第一个定义生效,宏展开为左侧代码;其余任何情况(未开启该 Feature,或目标不是 wasm32)都由第二个定义接管,展开为右侧代码。 - 宏解析机制:Rust 允许同名
macro_rules!在不同#[cfg]下各定义一个版本。这是"在宏层面模拟cfg!判断"的标准手法——它比cfg!更强,因为两个分支的代码可以分别携带各自才有的类型与依赖,编译器只对生效的那一支做类型检查与代码生成,未生效的一支完全不会进入编译。
该宏的典型价值体现在 Wasm 分包场景:开启分包后,路由与懒加载组件需要把"被延迟加载的模块"包装进wasm_split::LazyLoader;而不开启分包时,同样的位置只需直接调用原函数。两组代码语义差异极大,但借助maybe_wasm_split!,代码生成器可以在同一处调用点"无痛切换"。
四、调用链一:core-macro中懒加载组件如何消费该宏
#[component]宏的代码生成在 packages/core-macro/src/component.rs 中,我们可以看到maybe_wasm_split!的实际用法:
quote! { fn #lazy_name #generics (#anon_props) #fn_output #where_clause { #block } dioxus::config_macros::maybe_wasm_split! { if wasm_split { { static __MODULE: wasm_split::LazyLoader<#props_ty, #out_ty> = wasm_split::lazy_loader!(extern "lazy" fn #lazy_name(props: #props_ty,) -> #out_ty); use_resource(|| async move { __MODULE.load().await }).suspend()?; __MODULE.call(props).unwrap() } } else { { #lazy_name(props) } } } }这段代码展示了两种编译目标下的差异化实现:
- 未开启分包:直接调用
#lazy_name(props),即普通函数调用; - 开启分包:声明一个
wasm_split::LazyLoader静态变量,通过use_resource(...).suspend()?挂起组件直到懒加载模块下载完成,再用__MODULE.call(props)真正执行组件逻辑。
值得注意的是,maybe_wasm_split!是通过完整路径dioxus::config_macros::maybe_wasm_split!调用的,这正是 README 中"downstream codegen to use them under the 'dioxus' namespace"这句话的落地——下游宏展开出的代码引用的是dioxuscrate 的命名空间,而不是直接依赖dioxus-config-macros这个内部 crate,从而保证了命名空间的统一与稳定。
五、调用链二:router-macro的路由表代码生成
另一个重要的消费方是路由宏。在 packages/router-macro/src/route.rs 中,#[derive(Routable)]展开出的路由匹配臂同样包了一层maybe_wasm_split!:
quote! { #[allow(unused)] (#last_index, Self::#name { #(#dynamic_segments,)* }) => { dioxus::config_macros::maybe_wasm_split! { if wasm_split { { fn #comp_name(args: #router_name) -> Element { match args { #router_name::#name { #(#dynamic_segments_from_route_,)* } => { rsx! { #component { #(#dynamic_segments_from_route__: #dynamic_segments_from_route__,)* } } } _ => unreachable!() } } // ... 内部懒加载 LoaderInner 组件 } } else { // ... 直接渲染目标组件的路径 } } } }在开启 wasm-split 时,每个路由页面会被包装进独立的懒加载组件LoaderInner,配合 Suspense 按需下载对应页面的 Wasm 分块;未开启时则直接内联渲染。源码注释还透露了一个工程细节:路由宏生成的代码约占 docsite 二进制体积的 30%-40%,因此把"分包复杂度推向代码生成末端(leaf)"而不是核心,是当时的设计权衡。这也解释了为什么 Dioxus 特意为代码生成器维护了一套独立的配置宏基础设施——它承载的是对最终二进制体积有决定性影响的编译期决策。
六、dioxus-config-macro:九个平台宏与define_config_macro!批量化生成
如果说config-macros解决的是 wasm-split 的"二选一",那么config-macro解决的是"每个平台/模式一个开关"。其 src/lib.rs 用define_config_macro!这个内部辅助宏批量生成了全部九个过程宏:
macro_rules! define_config_macro { ($name:ident if $($cfg:tt)+) => { #[proc_macro] pub fn $name(input: TokenStream) -> TokenStream { if cfg!($($cfg)+) { let input = TokenStream2::from(input); quote! { { #input } } } else { quote! { () } } .into() } }; } define_config_macro!(server_only if any(feature = "ssr", feature = "liveview")); define_config_macro!(client if any(feature = "desktop", feature = "web", feature = "mobile")); define_config_macro!(web if feature = "web"); define_config_macro!(desktop if feature = "desktop"); define_config_macro!(native if feature = "native"); define_config_macro!(mobile if feature = "mobile"); define_config_macro!(fullstack if feature = "fullstack"); define_config_macro!(ssr if feature = "ssr"); define_config_macro!(liveview if feature = "liveview");define_config_macro!的实现非常直白:通过cfg!($($cfg)+)在编译期求值条件,命中时把传入的 token 原样包进一个代码块原样展开;未命中时则展开为()(空元组,不产生任何副作用)。因为cfg!是编译期常量求值,被丢弃的分支同样不会进入类型检查。
九个宏的语义可以整理为下表("命中"指该宏展开为传入的代码,"未命中"则展开为()):
| 宏 | 生效条件(Feature) | 典型用途 |
|---|---|---|
server_only! | ssr或liveview | 仅服务端/后端渲染场景注入服务端配置 |
client! | desktop、web或mobile任一 | 客户端侧代码块 |
web! | web | 仅 Web 平台 |
desktop! | desktop | 仅桌面平台(窗口、菜单等配置) |
native! | native | 仅原生 wgpu/winit 渲染器 |
mobile! | mobile | 仅移动端 |
fullstack! | fullstack | 全栈模式(前后端同仓构建) |
ssr! | ssr | 服务端渲染 |
liveview! | liveview | Liveview(WebSocket 驱动)模式 |
七、Feature 传递链路:从dioxus的 Cargo features 到宏生效
这套机制的关键在于"Feature 如何传导"。打开 packages/dioxus/Cargo.toml 可以看到,dioxus主 crate 的每个平台 Feature 都会同步点亮dioxus-config-macro的对应 Feature:
fullstack = [ "dep:dioxus-fullstack", "dioxus-config-macro/fullstack", # ... ] desktop = ["dep:dioxus-desktop", "dioxus-config-macro/desktop"] mobile = ["dep:dioxus-desktop", "dioxus-config-macro/mobile"] web = [ "dep:dioxus-web", "dioxus-fullstack?/web", "dioxus-config-macro/web", # ... ] ssr = ["dep:dioxus-ssr", "dioxus-config-macro/ssr"] liveview = ["dep:dioxus-liveview", "dioxus-config-macro/liveview"] native = ["dep:dioxus-native", "dioxus-config-macro/native"]而 wasm 分包相关的 Feature 同样会传导到两个 crate:
launch = ["dep:dioxus-config-macro"] wasm-split = [ "dep:wasm-splitter", "dioxus-config-macros/wasm-split", ] # note: to turn on the router splitter, you need to manually enable wasm-split on the router由此形成的完整链路是:
用户在 Cargo.toml 启用 dioxus 的 features(如 desktop / web / fullstack / wasm-split) │ ▼ dioxus 的 feature 定义中同步启用 dioxus-config-macro 或 dioxus-config-macros 的对应 feature │ ▼ 过程宏内部用 cfg!(...) 读取这些 feature,决定展开传入代码还是 () │ ▼ LaunchBuilder / core-macro / router-macro 生成的代码里,宏调用点拿到正确的平台实现值得注意的是一个细节:wasm-split在 packages/dioxus/Cargo.toml 中通过dioxus-config-macros/wasm-split传导,且注释明确提示"如果要启用路由器分包,需要手动在 router 上开启 wasm-split"。这说明宏的开关与对应运行时 crate 的开关是两套独立控制,用户需要同时点亮才能获得完整的分包效果。
八、实战:在LaunchBuilder中按平台条件注入配置
这些宏面向应用开发者最直观的用法,是配合dioxus::LaunchBuilder的平台配置注入。LaunchBuilder定义在 packages/dioxus/src/launch.rs,其with_cfg接受一个实现LaunchConfig的平台配置对象。问题在于:同一份启动代码如果同时面向桌面与 Web,desktop专属的窗口配置在 Web 上根本没有对应类型——这正是desktop!宏派上用场之处。
packages/dioxus/src/launch.rs 中给出了官方示例:
use dioxus::prelude::*; use dioxus_desktop::{Config, WindowBuilder}; dioxus::LaunchBuilder::new() .with_cfg(desktop! { Config::new().with_window( WindowBuilder::new() .with_title("My App") ) }) .launch(app);当启用desktopfeature 时,desktop!{ ... }展开为代码块,Config::new()被正常构造并注入 builder;当编译目标是 Web 时,整个块被替换成(),with_cfg收到一个空元组——由于with_cfg的泛型签名只要求impl LaunchConfig,()恰好不会对平台分派产生任何影响。
同样,服务端配置的场景在 packages/fullstack-server/src/config.rs 的文档示例中出现:
// Only set the server config if the server feature is enabled LaunchBuilder::new() .with_cfg(server_only!( dioxus_server::ServeConfig::default() .incremental(dioxus_server::IncrementalRendererConfig::default()) )) .launch(app);server_only!只有在ssr或liveviewfeature 启用时才展开ServeConfig配置,从而让"服务端配置"与"客户端构建"在同一个代码库中和平共处。仓库的 Playwright 集成测试也大量使用了这一模式,例如 packages/playwright-tests/fullstack-routing/src/main.rs、packages/playwright-tests/suspense-carousel/src/main.rs 中都能看到server_only!包裹的配置注入,这从测试侧验证了宏在实际项目中的可用形态。
九、为什么doc(hidden)且 semver-exempt:内部宏的契约设计
回看 packages/config-macros/src/lib.rs 中maybe_wasm_split!的注释:
Used by the internal router-macro code. The contents here are considered to be semver exempt.
两个关键设计信号:
#[doc(hidden)]:从docs.rs和 rustdoc 中隐藏,避免应用开发者误用,明确"这不是公共 API"。- semver-exempt(不受语义化版本约束):Dioxus 保留随时调整该宏签名与行为的权利,而不会被视为破坏性变更。这对"框架内部 codegen 工具"而言是务实的契约选择——它服务的是
core-macro、router-macro这些同仓库的代码生成器,属于 Dioxus 编译管线的一部分,而非面向生态的稳定接口。
这种"内部工具豁免"策略也解释了 README 的措辞:它不承诺任何稳定性,只为 Dioxus 自身的宏展开提供一处集中、可寻址(dioxus::config_macros::)的条件判断入口。
十、小结
dioxus-config-macros与dioxus-config-macro是 Dioxus 的编译期条件代码生成基础设施,README 明确定位其为"供 codegen 内部使用,不面向一般用户"。- 复数 crate 提供
maybe_wasm_split!,通过双定义 +#[cfg]实现 wasm32 且开启 wasm-split 时选取"左分支"、否则选取"右分支"的 token 级二选一,是路由懒加载与组件分包代码生成的开关。 - 单数 crate 提供
server_only!、web!、desktop!等九个过程宏,由define_config_macro!统一批量生成,命中 feature 时原样展开传入代码、未命中时展开为()。 - Feature 通过 packages/dioxus/Cargo.toml 的 feature 定义传导到两个宏 crate,最终在
dioxus::config_macros::命名空间与 prelude 中供代码生成器与开发者使用。 - 实际调用证据分布在 packages/core-macro/src/component.rs、packages/router-macro/src/route.rs、packages/dioxus/src/launch.rs 以及多个 Playwright 测试用例 中。
对于想要深入 Dioxus 内部机制、或者计划开发第三方渲染器与宏插件的读者,这套配置宏是理解"Feature 如何在编译期驱动代码生成"的极佳样板:它不依赖运行时反射,不引入额外依赖,仅凭cfg!与macro_rules!的两段式技巧,就把平台差异完整地消解在编译期。
- 前端
- 跨平台
- UI组件
- 桌面应用
- 移动开发
【免费下载链接】dioxus
Fullstack app framework for web, desktop, and mobile.
相关推荐
Gum:跨平台动态分析与代码生成利器
Gum:跨平台动态分析与代码生成利器 项目介绍 Gum 是一个用C语言编写的跨平台动态分析与代码生成库。它作为 Frida https://github.com
应用安全开发工具Dioxus 条件编译宏指南:用 dioxus-config-macro 管理跨平台 launch 配置
Dioxus 条件编译宏指南:用 dioxus config macro 管理跨平台 launch 配置 dioxus config macro 是 Dioxu
前端跨平台UI组件桌面应用移动开发代码生成器原理:ZLT平台自动化代码生成机制详解
代码生成器原理:ZLT平台自动化代码生成机制详解 在当今快速发展的软件开发领域,ZLT微服务平台通过其强大的 代码生成器 功能,为企业级应用开发带来了革命性的效
后端微服务认证鉴权
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考