Rust in Everything
基于 dioxus 0.7.10

一次请求的渲染

主线位置:首屏主线的服务端部分,从 axum 收到 GET / 到最后一块 HTML 写进响应体。一次页面请求已经按顺序走过这一段并标注了 curl 拿到的每一块 HTML,这一页不再重复那些标注,而是往下挖几处它只点到为止的地方:路由表里到底有什么、render_to 的每一步、本地线程池的大小和阻塞的代价、状态码和响应头的截止时刻、出错时渲染器的去向。HTML 本身怎么写见 dioxus-ssr,水合数据见水合。

flowchart TB
  launch["dioxus::launch"] --> sserver[serve_server]
  serve["dioxus::serve 闭包"] --> srouter[serve_router]
  sserver --> srouter
  routerfn["server::router app"] --> sda[serve_dioxus_application]
  sserver --> sda
  sda --> sfn["server function 路由"]
  sda --> assets["public 静态文件"]
  sda --> fb["fallback get render_handler"]
  fb --> rt["render_to"]
  rt --> pool["spawn_pinned 本地线程"]
  pool --> vd["新 VirtualDom"]
  vd --> status["根错误边界 定状态码"]
  status --> chunks["分块写 HTML"]

本页的实验都用同一个应用:一个 Routable 枚举挂着几个页面,各测一件事;三个 #[get] server function。用 dx build --platform web --fullstack --release(dx 0.7.9,库 0.7.10)构建,IP=127.0.0.1 PORT=18790 ./server 启动。平台是 M2 Max(12 核)、macOS 27、rustc 1.99.0。各页面的代码在用到它的小节里给出。

三个入口,一张路由表

服务端有三种起法,最后都落到同一个方法上:

写法走到哪用的 ServeConfig
dioxus::launch(app) / LaunchBuilder::launchlaunch_cfg 建一个 tokio 多线程运行时,block_on serve_serverwith_cfg 传进来的,没有就 ServeConfig::new()
`dioxus::serve(async { Ok(router) })`
dioxus::server::router(app)router 只返回一个 axum::Router,通常放在 serve 的闭包里,再往上加自己的路由和中间件固定 ServeConfig::new()

serve_server 和 router 里都是这一行,外面再套一层 apply_base_path(配了 base path 时把整张表 nest 到 /{base}/ 下):

                Router::new().serve_dioxus_application(cfg.clone(), original_root),

serve_router 先初始化日志、绑端口,然后分两路:release 构建直接 axum::serve(listener, router);debug 构建自己写一个 accept 循环,同时 select! 等 dx 发来的热补丁消息,收到就重建整张路由表,并通知已有连接关闭。热补丁本身不在本卷范围内,这里只需要知道 release 下就是普通的 axum::serve。

serve_dioxus_application 是三步:

        self.register_server_functions()
            .serve_static_assets()
            .fallback(get(FullstackState::render_handler))
            .with_state(FullstackState::new(cfg, app))

server function 路由

register_server_functions 遍历 inventory 收集到的每个 ServerFunction,用 "{method} {path}" 去重,再 route(path, method_router)。实验应用启动时的日志就是这三行:

2026-10-28T11:54:35.354429Z  INFO dioxus_server::server: Registering: GET /api/sleep
2026-10-28T11:54:35.354986Z  INFO dioxus_server::server: Registering: GET /api/word
2026-10-28T11:54:35.354999Z  INFO dioxus_server::server: Registering: GET /api/num

宏怎么把函数登记进 inventory、handler 里怎么解码参数,见 server function。

静态文件

serve_static_assets 先找 public 目录(public_path:环境变量 DIOXUS_PUBLIC_PATH,否则是可执行文件旁边的 public/),然后 serve_dir_cached 在启动时把目录读一遍:

  • index.html 跳过,它由 fallback 渲染出来。
  • 文件:每个文件一条 nest_service 路由,ServeFile::new(&path).precompressed_br()(有 .br 就发压缩版)。文件名形如 名字-dxh<十六进制>.扩展名 的,再包一层 cache_response_forever,加 cache-control: public, max-age=31536000, immutable。判断就是 file_name_looks_immutable:-dxh 之后到第一个 . 为止全是十六进制数字。
  • 子目录:release 递归下去,同样逐个文件注册;debug 直接 nest_service(&route, ServeDir::new(&path)),请求到来时才查文件系统。

release 构建里 public/ 是 index.html 加 assets/ 下两个带 hash 的文件,curl 其中的 JS:

HTTP/1.1 200 OK
content-type: text/javascript
accept-ranges: bytes
last-modified: Wed, 28 Oct 2026 11:54:28 GMT
content-length: 67551
cache-control: public, max-age=31536000, immutable

debug 构建的文件在 public/wasm/ 下,名字不带 hash(exp-dioxus-fs1-fs.js),走 ServeDir,响应里没有 cache-control:

HTTP/1.1 200 OK
content-type: text/javascript
accept-ranges: bytes
last-modified: Wed, 28 Oct 2026 11:58:17 GMT
content-length: 121870

(两段都去掉了 date 行。)

📝NOTE
冷知识:带 hash 的文件永久缓存,是 0.7 才有的

issue #3278「Web app resources are not cached」抱怨:第二次打开页面时,wasm 和 JS 一个字节都没重新下载,浏览器却对每个文件发 if-modified-since、等一个 304 Not Modified,光这一来一回就花了 136 毫秒。PR #3928(2025-06-20 合并,首次随 v0.7.0-alpha.2 发布)关掉了这个 issue。PR 说明里的方案是让 CLI 写一个 JSON 文件,告诉服务端哪些资源带了 hash;合并时的提交列表里有一条「look for hash in asset name instead of building a manifest」,最后落地的就是上面那个只看文件名的 file_name_looks_immutable。debug 下子目录改用 ServeDir 是之后的 PR #4983(首次随 v0.7.3 发布),原因是热补丁会往 public/wasm/ 里写新文件,启动时注册好的路由表里没有它们,浏览器请求补丁拿到 404。

fallback

剩下的所有路径都进 get(FullstackState::render_handler)。外面包着 get,所以别的方法打到页面路径上是 405;路径就算看起来像文件,只要启动时没注册,也会被当成一次页面渲染:

$ curl --noproxy '*' -s -o /dev/null -w '%{http_code}\n' -X POST http://127.0.0.1:18790/
405
$ curl --noproxy '*' -sI http://127.0.0.1:18790/index.html
HTTP/1.1 404 Not Found
content-length: 779

/index.html 的 404 不是静态文件服务给的,而是 router 解析不了 /index.html 这个路由(下面「状态码」一节)。axum 那一侧这张表怎么合并、fallback 怎么挂,见 axum 卷的 dioxus 生成的 Router。

FullstackState:一个线程池,一个渲染器池

with_state 的 FullstackState::new 造两样东西,之后每个请求共享:

        let rt = LocalPoolHandle::new(
            std::thread::available_parallelism()
                .map(usize::from)
                .unwrap_or(1),
        );

        Self {
            renderers: Arc::new(SsrRendererPool::new(4, config.incremental.clone())),
            build_virtual_dom: Arc::new(move || VirtualDom::new_with_props(root.clone(), ())),
            config,
            rt,
        }
  • rt:tokio-util 的 LocalPoolHandle,线程数是 available_parallelism,这台机器上是 12。它和 axum 跑在上面的那个 tokio 多线程运行时是两套线程。
  • renderers:SsrRendererPool,一个 RwLock<Vec<Renderer>>,预先放 4 个 pre_render = true 的渲染器,外加可选的增量缓存。
  • build_virtual_dom:每次调用都 VirtualDom::new_with_props,所以每个请求一棵全新的树。

每个 FullstackState 都有自己的线程池。apply_base_path 为 base path 的根路径又 FullstackState::new 了一次,配了 base path 的应用就有两个池;dioxus::server::router 每调一次也是一个新池。

render_handler 只留下请求的 Parts,请求体直接丢掉;工厂闭包在 build_virtual_dom 之外,再把 ServeConfig 里的每个 context_providers 塞进根 context。

render_to 逐步

SsrRendererPool::render_to 运行在 axum 的 handler 里(多线程运行时),它自己不渲染,只准备好通道、把渲染交出去、等一个结果:

  1. 取 parts.uri.path_and_query() 作路由字符串,取不到就是 400。
  2. 建两条通道:容量 1000 的 mpsc,渲染那边往里写 HTML 块,handler 把接收端当响应体;一个 oneshot,渲染那边决定好状态码和响应头后发一次。
  3. check_cached_route:配了增量缓存且命中,把整页字符串塞进通道,直接返回 200,不碰线程池(见增量渲染与配置)。
  4. 从渲染器池 pop 一个;池空了就 pre_renderer 现造一个。
  5. 把剩下的整个渲染过程包成闭包 create_render_future,rt.spawn_pinned(create_render_future)。
  6. initial_result_rx.await:等那个 oneshot。拿到 Ok((status, headers)) 就返回,响应体是一个包着接收端的 ReceiverWithDrop;拿到 Err 就是错误响应。

ReceiverWithDrop 的 Drop 会 abort 那个渲染任务:客户端断开、axum 丢掉响应体时,本地线程上的渲染也就停了。

闭包在本地线程上做的事,按顺序:

步骤做什么本卷哪里讲
建树virtual_dom_factory(),根上放 ServerDocument、FullstackContext::new(parts)、MemoryHistory、可水合的错误边界工厂,rebuild_in_place一次页面请求
等Disabled:wait_for_suspense().await 等完所有挂起;OutOfOrder:循环 wait_for_suspense_work / render_suspense_immediate,直到 router 提交了首块或没有挂起任务Suspense 与错误边界、流式渲染
定状态码查根错误边界,有错就把错误发进 oneshot 并 return;没错就发 current_http_status() 和 take_response_headers()本页下一节
写头render_head 写 index.html 的 <head> 和 ServerDocument 收集的元素,StreamingRenderer::new 把它作为第一块送出document 与 eval
写主体renderer.pre_render = true,装上流式回调,renderer.render(&virtual_dom),接着 render_after_main 写水合数据和模板剩下的部分,作为第二块送出dioxus-ssr、水合
流式补块while suspended_tasks_remaining():每个完成的边界发一段替换块流式渲染
收尾render_after_body 写 </body> 之后的模板;有增量缓存就把整页(不带流式的部分)重新渲染一遍存起来;送出最后一块增量渲染与配置
还渲染器reset_render_components(),push 回池子本页最后一节

本地线程池:为什么、有几个、阻塞挡住谁

VirtualDom 里到处是 Rc 和 RefCell(Runtime 本身就是 Rc 共享的,见 VirtualDom 与 Runtime),它不是 Send,渲染它的 future 也不是。tokio 的多线程调度器会在线程之间搬任务,tokio::spawn 要求 Send;spawn_pinned 只要求「造 future 的闭包」是 Send,闭包被送到池里某个线程上调用,造出的 future 从此只在那个线程上 poll。create_render_future 捕获的东西(Parts、ServeConfig、渲染器、通道的发送端、工厂闭包)都是 Send,VirtualDom 是在闭包里面才新建的。

池里每个线程跑一个只属于自己的单线程调度器。具体是哪一种取决于你的 Cargo.lock 里的 tokio-util 版本(dioxus 只要求 0.7.15 以上):0.7.15 到 0.7.19 是 new_current_thread() 运行时上套一个 LocalSet,0.7.20 改成了 tokio 的 LocalRuntime。本页实验的 lock 里是 0.7.20,本站的 lock 里是 0.7.18。两者都是「一个线程、一个只跑本线程任务的调度器」,下面的结论不受影响(LocalSet 和 LocalRuntime 本身见 tokio 卷的 JoinSet、LocalSet、task_local)。

选线程的规则是 find_and_incr_least_burdened_worker:每个线程有一个原子计数器,记着它身上还没结束的任务数,挑最小的那个,compare_exchange 加一;任务结束时减一。

这就决定了阻塞的代价。实验里两个页面,一个在渲染时同步睡 500 毫秒,一个在 use_server_future 里异步等 500 毫秒(sleep_ms 是一个 #[get] server function,函数体里 tokio::time::sleep),都把自己所在线程的 id 写进页面:

#[cfg(feature = "server")]
fn tid() -> String {
  format!("{:?}", std::thread::current().id())
}

#[component]
fn Block() -> Element {
  // blocking the local thread while rendering
  let t = tid();
  #[cfg(feature = "server")]
  {
    dioxus::logger::tracing::info!("block start on {t}");
    std::thread::sleep(std::time::Duration::from_millis(500));
    dioxus::logger::tracing::info!("block end   on {t}");
  }
  rsx! { p { "blocked on {t}" } }
}

#[component]
fn Wait() -> Element {
  // awaiting instead of blocking
  let t = tid();
  let v = use_server_future(|| sleep_ms(500))?;
  rsx! { p { "waited {v():?} on {t}" } }
}

同时发 16 个 curl,记下每个的 time_total(排好序),再统计页面里的线程 id:

== /block x16
0.500887 0.501934 0.502850 0.504470 0.505052 0.505404 0.505703 0.505716 0.505824 0.505831 0.506109 0.506506 0.995513 0.998171 0.999937 1.001352 
== /wait x16
0.502816 0.502847 0.502854 0.503181 0.503245 0.503246 0.503463 0.503597 0.503646 0.503935 0.504181 0.504255 0.504327 0.504506 0.505535 0.507546 
   2 blocked on ThreadId(17)
   2 blocked on ThreadId(16)
   2 blocked on ThreadId(15)
   2 blocked on ThreadId(14)
   1 blocked on ThreadId(25)
   1 blocked on ThreadId(24)
   1 blocked on ThreadId(23)
   1 blocked on ThreadId(22)
   1 blocked on ThreadId(21)
   1 blocked on ThreadId(20)
   1 blocked on ThreadId(19)
   1 blocked on ThreadId(18)

/wait 那一组的线程分布一模一样(14 到 17 各两次,18 到 25 各一次),时间却全是 0.5 秒。读法:

  • 一共 12 个线程(ThreadId(14) 到 ThreadId(25)),等于 available_parallelism。
  • 前 12 个请求各分到一个空闲线程;第 13 到 16 个请求到来时每个线程的计数都是 1,只能排到已经有任务的线程上。
  • 同步阻塞时,排在后面的那 4 个要等前一个睡完才开始,所以是 1 秒。异步等待时,线程在 .await 处让出,同一线程上的两个渲染交替进行,都是 0.5 秒。

阻塞挡住的不止是同一页面。再做一次:先发 12 个 /block 占满所有线程,100 毫秒后依次请求首页、一个静态文件、一个 server function,最后等全部结束再单独请求一次首页:

home during 12 blocks: 0.390720
asset during 12 blocks: 0.000768
server fn during 12 blocks: 0.000602
home alone: 0.000645

首页本身什么也不做,平时 0.6 毫秒,这次等了 0.39 秒:它被分到了一个正在睡的线程上,等那次 /block 睡完(500 毫秒减去先睡掉的 100 毫秒)才开始渲染。静态文件不受影响,它走的是 axum 的多线程运行时,不进本地线程池。server function 这里看起来也很快,但那只是因为请求是依次发的:首页那 0.39 秒已经把 /block 等完了。server function 的 handler 其实也进同一个池,make_handler 里是 state.rt.spawn_pinned(...),注释写着「Allow !Send futures by running in the render handlers pinned local pool」;把首页、静态文件和 server function 同时发出去,server function 也要等 0.39 秒,只有静态文件是 1 毫秒以内(实验见设计取舍)。所以服务端渲染里做同步 IO、std::thread::sleep、算很久的东西,卡住的是池里的一个线程以及之后被分到那个线程的页面请求和 server function 调用(和 tokio 卷在 async 里阻塞是同一件事,只是这里每个线程是一个单线程调度器,没有别的 worker 来偷任务)。

首页单独请求是 0.6 毫秒,这里面包括新建一个 VirtualDom、跑一遍组件树、写 HTML:每个请求都从根开始跑,请求之间不共享任何组件状态。

状态码和响应头:在首块之前定下

闭包等完 suspense 之后,先看根错误边界(ScopeId::ROOT_ERROR_BOUNDARY)里有没有错误:

            // check if there are any errors from the root error boundary
            let error = virtual_dom.in_scope(ScopeId::ROOT_ERROR_BOUNDARY, || {
                consume_context::<ErrorContext>().error()
            });

有错误时依次尝试几种类型,后面的覆盖前面的:

错误里是状态码响应体
HttpError { status, message }statusmessage,没有就是状态码的标准短语
http::StatusCode它自己标准短语
ServerFnError::ServerError { code, message, .. }code,不是合法状态码就 500message
router 的 ParseRouteError404解析失败的说明
以上都不是500错误的 to_string(),服务端打一行 Failed to render page

挑到状态码就把 SSRError::HttpError 发进 oneshot,return。handler 那边用 SSRError 拼一个只有状态码和一段纯文本的响应:整页都不渲染,没有 <html>,也没有 content-type。实验应用的两个页面:

#[component]
fn Teapot() -> Element {
  HttpError::im_a_teapot("short and stout")?;
  rsx! { p { "unreachable" } }
}
$ curl --noproxy '*' -s -i http://127.0.0.1:18790/teapot
HTTP/1.1 418 I'm a teapot
content-length: 15

short and stout
$ curl --noproxy '*' -s -i http://127.0.0.1:18790/nope
HTTP/1.1 404 Not Found
content-length: 737

Failed to parse route Route did not match:
Attempted Matches:
1) Route 'Home' ('/') did not match:
Found additional trailing segments: nope
2) Route 'Block' ('/block') did not match:
Static segment 'block' did not match instead found 'nope'
...
7) Route 'Swap2' ('/swap2') did not match:
Static segment 'swap2' did not match instead found 'nope'

(去掉了 date 行,404 的正文中间删了几行。)/nope 这种路径进来,router 的 RouterContext::current 解析失败,throw_error(ParseRouteError { .. }),错误一路冒到根错误边界,于是变成 404,正文是 ParseRouteError 的消息:把路由枚举的每个变体和它的路径都列了出来,原样发给了访问者。只有冒到根错误边界的错误才变状态码。把同一个 Teapot 包进自己的 ErrorBoundary:

#[component]
fn Caught() -> Element {
  rsx! {
    ErrorBoundary { handle_error: |e: ErrorContext| rsx! { p { "caught: {e.error().unwrap()}" } },
      Teapot {}
    }
  }
}

响应就是一个普通页面,状态码 200,错误消息只出现在 HTML 里:

HTTP/1.1 200 OK
content-type: text/html; charset=utf-8
...
</script><p data-node-hydration="0"><!--node-id1-->caught: 418 I&#39;m a teapot: short and stout<!--#--></p><script>window.initial_dioxus_hydration_data=...

没有错误时,状态码和响应头来自 FullstackContext:

            // Check the FullstackContext in case the user set the statuscode manually or via a layout.
            let http_status = streaming_context.current_http_status();
            let headers = streaming_context
                .take_response_headers()
                .unwrap_or_default();

            // Now that we handled any errors from rendering, we can send the initial ok result
            _ = initial_result_tx.send(Ok((http_status, headers)));

组件或 server function 的函数体里可以 FullstackContext::commit_http_status 改状态码、add_response_header 加响应头。take_response_headers 是 Option::take:发完这一次,response_headers 就是 None,之后的 add_response_header 找不到 HeaderMap,什么也不做;状态码也只读这一次。截止时刻就是这行 send,而它在哪取决于流式模式:Disabled 下在所有 suspense 完成之后,OutOfOrder 下在首块之前(router 调了 commit_initial_chunk,或者已经没有挂起的任务)。

实验页面在首次渲染时加一个头,在一个 suspense 边界里的 use_server_future 等完 300 毫秒之后再加一个头、把状态码改成 201:

#[component]
fn Hdr() -> Element {
  #[cfg(feature = "server")]
  if let Some(cx) = dioxus::fullstack::FullstackContext::current() {
    cx.add_response_header(
      dioxus::fullstack::http::HeaderName::from_static("x-early"),
      dioxus::fullstack::http::HeaderValue::from_static("1"),
    );
  }
  rsx! {
    p { "header page" }
    SuspenseBoundary { fallback: |_| rsx! { "loading" },
      Late {}
    }
  }
}

#[component]
fn Late() -> Element {
  let v = use_server_future(|| async move {
    let out = sleep_ms(300).await;
    // runs after the component suspended once
    #[cfg(feature = "server")]
    if let Some(cx) = dioxus::fullstack::FullstackContext::current() {
      cx.add_response_header(
        dioxus::fullstack::http::HeaderName::from_static("x-late"),
        dioxus::fullstack::http::HeaderValue::from_static("1"),
      );
      dioxus::fullstack::FullstackContext::commit_http_status(
        dioxus::fullstack::http::StatusCode::CREATED,
        None,
      );
    }
    out
  })?;
  rsx! { p { "late {v():?}" } }
}

默认配置下 curl 的响应头(去掉 date):

HTTP/1.1 201 Created
content-type: text/html; charset=utf-8
age: 0
x-early: 1
x-late: 1
transfer-encoding: chunked

同一个二进制,main 里读到环境变量就给 ServeConfig 加 enable_out_of_order_streaming(),换成乱序流式再请求一次:

HTTP/1.1 200 OK
content-type: text/html; charset=utf-8
age: 0
x-early: 1
transfer-encoding: chunked

Disabled 下,等 suspense 发生在定状态码之前,晚到的头和状态码都赶上了。OutOfOrder 下,首块在 Late 还挂着时就发出去了,那时响应头已经被 take 走,x-late 和 201 都悄无声息地丢了,没有警告。(age: 0 是 RenderFreshness::write 写的,增量缓存那一页讲。)

渲染器什么时候回池

渲染器在第 4 步被 pop 出来,move 进了 create_render_future。整段闭包里只有一处把它还回去,就在最后:

            stream.render(post_streaming);

            renderer.reset_render_components();
            myself.renderers.write().unwrap().push(renderer);

中途任何一个 return 都绕过了这一行:根错误边界里有错误(上面的 418、404、500 全在此列)、render_head 失败、throw_error! 宏(写主体、写尾部、流式替换块、写增量缓存失败时)。渲染任务被 ReceiverWithDrop 中途 abort 时也一样。这些情况下渲染器随闭包一起被 drop。

不会因此卡住:池空了,第 4 步的 unwrap_or_else(Self::pre_renderer) 会现造一个新的。丢掉的是那个渲染器的 template_cache,新渲染器要从头切每个模板的字符串段。所以一串 404(比如扫描器乱请求路径)会把池子里攒下的渲染器一个个消耗掉,之后的正常请求换成新渲染器、重新切模板。池子的大小也就不是固定的 4:并发高时现造的渲染器正常结束后都会 push 回去,池子最多涨到同时成功渲染的请求数,出错的请求再把它往下减。这一点在源码之外观察不到(池子是私有的),以上是读代码得出的。

❗IMPORTANT
动脑筋:同时发 16 个阻塞 500 毫秒的页面请求,有几个要等 1 秒?

上面的 Block 页面,在渲染时 std::thread::sleep 500 毫秒。服务端刚启动、没有别的请求,16 个 curl 同时发出。M2 Max(available_parallelism 是 12)、macOS 27、release 构建、tokio-util 0.7.20。几个请求的 time_total 接近 1 秒?

动脑筋答案

那次运行的 16 个时间(排序后,原样):

0.500887 0.501934 0.502850 0.504470 0.505052 0.505404 0.505703 0.505716 0.505824 0.505831 0.506109 0.506506 0.995513 0.998171 0.999937 1.001352 

4 个。池里 12 个线程,spawn_pinned 每次挑计数最小的线程:前 12 个请求各占一个,计数都变成 1;后 4 个请求只能排到计数为 1 的线程上,而那个线程正被 std::thread::sleep 占着,一直等到前一个渲染结束才开始自己的 500 毫秒。页面里的线程 id 也印证了这一点:ThreadId(14) 到 ThreadId(17) 各出现两次。

小结

  • launch、serve、server::router 最后都调 serve_dioxus_application:先挂 server function 路由,再把 public/ 里除 index.html 外的文件逐个挂上(release 下 -dxh hash 文件名加一年的 immutable 缓存,debug 下子目录走 ServeDir),其余路径进 get(render_handler)。
  • FullstackState 持有一个 available_parallelism 个线程的 LocalPoolHandle 和一个初始 4 个渲染器的池;render_to 在 axum 的线程上只建通道、spawn_pinned、等 oneshot,渲染整个在本地线程上做。
  • spawn_pinned 挑任务最少的线程;渲染里同步阻塞会让之后分到同一线程的页面请求和 server function 排队,只有静态文件不受影响。
  • 根错误边界里的错误决定状态码,响应只有状态码和一段纯文本;ParseRouteError 的 404 正文会列出全部路由。没有错误时状态码和响应头只在发首块前读一次,OutOfOrder 下之后再设置会静默丢失。
  • 渲染器只在成功走到最后时回池;出错或被中止时渲染器连同模板缓存一起丢掉,池子按需现造新的。

💬 关联讨论

发起讨论
加载中...