Skip to content

Commit 2c61622

Browse files
committed
feat: add yew-link crate for unified SSR/CSR data fetching
1 parent 0dd4d03 commit 2c61622

24 files changed

Lines changed: 2150 additions & 405 deletions

File tree

Cargo.lock

Lines changed: 384 additions & 327 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,3 +59,4 @@ chrono = "0.4"
5959
thiserror = "2.0"
6060
bincode = { version = "2.0.0-rc.3", features = ["serde"] }
6161
reqwest = "0.13"
62+
axum = "0.8"

examples/function_router/src/content.rs

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1+
use serde::{Deserialize, Serialize};
2+
13
use crate::generator::{Generated, Generator};
24

3-
#[derive(Clone, Debug, Eq, PartialEq)]
5+
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
46
pub struct Author {
57
pub seed: u32,
68
pub name: String,
@@ -22,7 +24,7 @@ impl Generated for Author {
2224
}
2325
}
2426

25-
#[derive(Clone, Debug, Eq, PartialEq)]
27+
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
2628
pub struct PostMeta {
2729
pub seed: u32,
2830
pub title: String,
@@ -48,7 +50,7 @@ impl Generated for PostMeta {
4850
}
4951
}
5052

51-
#[derive(Clone, Debug, Eq, PartialEq)]
53+
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
5254
pub struct Post {
5355
pub meta: PostMeta,
5456
pub content: Vec<PostPart>,
@@ -68,7 +70,7 @@ impl Generated for Post {
6870
}
6971
}
7072

71-
#[derive(Clone, Debug, Eq, PartialEq)]
73+
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
7274
pub enum PostPart {
7375
Section(Section),
7476
Quote(Quote),
@@ -87,7 +89,7 @@ impl Generated for PostPart {
8789
}
8890
}
8991

90-
#[derive(Clone, Debug, Eq, PartialEq)]
92+
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
9193
pub struct Section {
9294
pub title: String,
9395
pub paragraphs: Vec<String>,
@@ -112,7 +114,7 @@ impl Generated for Section {
112114
}
113115
}
114116

115-
#[derive(Clone, Debug, Eq, PartialEq)]
117+
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
116118
pub struct Quote {
117119
pub author: Author,
118120
pub content: String,

examples/function_router/src/lib.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,11 @@
1414
// Hence, it may not yield the same value on the client and server side.
1515

1616
mod app;
17-
mod components;
18-
mod content;
17+
pub mod components;
18+
pub mod content;
1919
mod generator;
2020
pub mod imagegen;
21-
mod pages;
21+
pub mod pages;
2222

2323
pub use app::*;
2424
pub use content::*;

examples/ssr_router/Cargo.toml

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@ name = "ssr_router"
33
version = "0.1.0"
44
edition = "2021"
55

6-
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
7-
86
[[bin]]
97
name = "ssr_router_hydrate"
108
required-features = ["hydration"]
@@ -15,8 +13,14 @@ required-features = ["ssr"]
1513

1614
[dependencies]
1715
yew = { path = "../../packages/yew" }
16+
yew-link = { path = "../../packages/yew-link" }
17+
yew-router = { path = "../../packages/yew-router" }
1818
function_router = { path = "../function_router" }
1919
log = "0.4"
20+
rand = { workspace = true }
21+
getrandom = { workspace = true }
22+
serde = { workspace = true, features = ["derive"] }
23+
serde_json = { workspace = true }
2024
futures = { workspace = true, features = ["std"] }
2125
hyper-util = "0.1.20"
2226

@@ -25,9 +29,8 @@ wasm-bindgen-futures.workspace = true
2529
wasm-logger.workspace = true
2630

2731
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
28-
yew-router = { path = "../../packages/yew-router" }
2932
tokio = { workspace = true, features = ["macros", "rt-multi-thread", "net", "fs"] }
30-
axum = "0.8"
33+
axum = { workspace = true }
3134
tower = { version = "0.5", features = ["make"] }
3235
tower-http = { version = "0.6", features = ["fs", "cors"] }
3336
env_logger = "0.11"
@@ -39,12 +42,15 @@ jemallocator = "0.5"
3942

4043
[target.'cfg(target_arch = "wasm32")'.dev-dependencies]
4144
wasm-bindgen-test.workspace = true
45+
wasm-bindgen.workspace = true
46+
web-sys = { workspace = true }
4247
gloo = { workspace = true }
4348
ssr-e2e-harness = { path = "../../tools/ssr-e2e-harness" }
4449

4550
[dev-dependencies]
46-
yew = { path = "../../packages/yew", features = ["hydration"] }
51+
yew = { path = "../../packages/yew", features = ["hydration", "test"] }
52+
yew-link = { path = "../../packages/yew-link", features = ["hydration"] }
4753

4854
[features]
49-
ssr = ["yew/ssr"]
50-
hydration = ["yew/hydration"]
55+
ssr = ["yew/ssr", "yew-link/ssr", "yew-link/axum"]
56+
hydration = ["yew/hydration", "yew-link/hydration"]
Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
1-
use function_router::App;
1+
use ssr_router::{App, AppProps, LINK_ENDPOINT};
22

33
fn main() {
44
#[cfg(target_arch = "wasm32")]
55
wasm_logger::init(wasm_logger::Config::new(log::Level::Trace));
6-
yew::Renderer::<App>::new().hydrate();
6+
yew::Renderer::<App>::with_props(AppProps {
7+
endpoint: LINK_ENDPOINT.into(),
8+
})
9+
.hydrate();
710
}

examples/ssr_router/src/bin/ssr_router_server.rs

Lines changed: 38 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,19 +9,23 @@ use axum::extract::{Query, Request, State};
99
use axum::handler::HandlerWithoutStateExt;
1010
use axum::http::Uri;
1111
use axum::response::IntoResponse;
12-
use axum::routing::get;
12+
use axum::routing::{get, post};
1313
use axum::Router;
1414
use clap::Parser;
15-
use function_router::{route_meta, Route, ServerApp, ServerAppProps};
15+
use function_router::{route_meta, Route};
1616
use futures::stream::{self, StreamExt};
1717
use hyper::body::Incoming;
1818
use hyper_util::rt::TokioIo;
1919
use hyper_util::server;
20+
use ssr_router::{
21+
LinkedAuthor, LinkedPost, LinkedPostMeta, ServerApp, ServerAppProps, LINK_ENDPOINT,
22+
};
2023
use tokio::net::TcpListener;
2124
use tower::Service;
2225
use tower_http::cors::CorsLayer;
2326
use tower_http::services::ServeDir;
2427
use yew::platform::Runtime;
28+
use yew_link::{linked_state_handler, Resolver, ResolverProp};
2529
use yew_router::prelude::Routable;
2630

2731
// We use jemalloc as it produces better performance.
@@ -46,25 +50,36 @@ fn head_tags_for(path: &str) -> String {
4650
)
4751
}
4852

53+
#[derive(Clone)]
54+
struct AppState {
55+
index_html_before: String,
56+
index_html_after: String,
57+
resolver: ResolverProp,
58+
}
59+
4960
async fn render(
5061
url: Uri,
5162
Query(queries): Query<HashMap<String, String>>,
52-
State((index_html_before, index_html_after)): State<(String, String)>,
63+
State(state): State<AppState>,
5364
) -> impl IntoResponse {
5465
let path = url.path().to_owned();
5566

5667
// Inject route-specific <head> tags before </head>, outside of Yew rendering.
57-
let before = index_html_before.replace("</head>", &format!("{}</head>", head_tags_for(&path)));
68+
let before = state
69+
.index_html_before
70+
.replace("</head>", &format!("{}</head>", head_tags_for(&path)));
71+
let resolver = state.resolver.clone();
5872

5973
let renderer = yew::ServerRenderer::<ServerApp>::with_props(move || ServerAppProps {
6074
url: path.into(),
6175
queries,
76+
resolver,
6277
});
6378

6479
Body::from_stream(
6580
stream::once(async move { before })
6681
.chain(renderer.render_stream())
67-
.chain(stream::once(async move { index_html_after }))
82+
.chain(stream::once(async move { state.index_html_after }))
6883
.map(Result::<_, Infallible>::Ok),
6984
)
7085
}
@@ -94,35 +109,44 @@ where
94109
#[tokio::main]
95110
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
96111
let exec = Executor::default();
97-
98112
env_logger::init();
99-
100113
let opts = Opt::parse();
101114

115+
let resolver_prop: ResolverProp = Resolver::new()
116+
.register_linked::<LinkedPost>(())
117+
.register_linked::<LinkedAuthor>(())
118+
.register_linked::<LinkedPostMeta>(())
119+
.into();
120+
let arc_resolver = resolver_prop.0.clone();
121+
102122
let index_html_s = tokio::fs::read_to_string(opts.dir.join("index.html"))
103123
.await
104124
.expect("failed to read index.html");
105125

106126
let (index_html_before, index_html_after) = index_html_s.split_once("<body>").unwrap();
107127
let mut index_html_before = index_html_before.to_owned();
108128
index_html_before.push_str("<body>");
109-
110129
let index_html_after = index_html_after.to_owned();
111130

131+
let app_state = AppState {
132+
index_html_before,
133+
index_html_after,
134+
resolver: resolver_prop,
135+
};
136+
112137
let app = Router::new()
138+
.route(
139+
LINK_ENDPOINT,
140+
post(linked_state_handler).with_state(arc_resolver),
141+
)
113142
.fallback_service(
114143
ServeDir::new(opts.dir)
115144
.append_index_html_on_directories(false)
116-
.fallback(
117-
get(render)
118-
.with_state((index_html_before.clone(), index_html_after.clone()))
119-
.into_service(),
120-
),
145+
.fallback(get(render).with_state(app_state).into_service()),
121146
)
122147
.layer(CorsLayer::permissive());
123148

124149
let addr: SocketAddr = ([0, 0, 0, 0], 8080).into();
125-
126150
println!("You can view the website at: http://localhost:8080/");
127151

128152
let listener = TcpListener::bind(addr).await?;

0 commit comments

Comments
 (0)