Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 3 additions & 6 deletions packages/yew-router/src/macro_helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ pub fn encode_path_for_url(path: &str) -> String {

use std::collections::HashMap;

use crate::utils::strip_slash_suffix;
use crate::Routable;

// re-export Router because the macro needs to access it
Expand All @@ -18,19 +17,17 @@ pub type Router = matchit::Router<String>;
/// Build a `matchit::Router` from a `Routable` type.
pub fn build_router<R: Routable>() -> Router {
let mut router = Router::new();
R::routes().iter().for_each(|path| {
let stripped_route = strip_slash_suffix(path);
R::routes().iter().for_each(|&path| {
router
.insert(stripped_route, path.to_string())
.unwrap_or_else(|e| panic!("failed to insert route {stripped_route:?}: {e}"));
.insert(path, path.to_string())
.unwrap_or_else(|e| panic!("failed to insert route {path:?}: {e}"));
});

router
}

/// Use a `matchit::Router` to match the route of a `Routable`
pub fn recognize_with_router<R: Routable>(router: &Router, pathname: &str) -> Option<R> {
let pathname = strip_slash_suffix(pathname);
let matched = router.at(pathname);

match matched {
Expand Down
52 changes: 49 additions & 3 deletions packages/yew-router/tests/router_unit_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,12 @@ fn router_trailing_slash() {
}),
AppRoute::recognize("/category/cooking-recipes/")
);

// Without trailing slash does NOT match a route defined with trailing slash
assert_eq!(
Some(AppRoute::NotFound),
AppRoute::recognize("/category/cooking-recipes")
);
}

#[test]
Expand All @@ -71,7 +77,7 @@ fn router_url_encoding() {
Some(AppRoute::Search {
query: "a/b".to_string()
}),
AppRoute::recognize("/search/a%2Fb/")
AppRoute::recognize("/search/a%2Fb")
);
}

Expand Down Expand Up @@ -154,9 +160,9 @@ fn router_nested() {
MainRoute::recognize("/settings")
);

// Trailing slash also matches root via strip_slash_suffix
// Trailing slash is now distinct from no trailing slash
assert_eq!(
Some(MainRoute::SettingsRoot),
Some(MainRoute::NotFound),
MainRoute::recognize("/settings/")
);

Expand Down Expand Up @@ -208,3 +214,43 @@ fn router_nested() {
MainRoute::recognize("/other/path")
);
}

#[test]
fn router_trailing_slash_distinct_routes() {
#[derive(Routable, Debug, Clone, PartialEq)]
enum AppRoute {
#[at("/")]
Home,
#[at("/about")]
AboutNoSlash,
#[at("/about/")]
AboutWithSlash,
#[at("/404")]
#[not_found]
NotFound,
}

// Each form matches only its own variant
assert_eq!(Some(AppRoute::AboutNoSlash), AppRoute::recognize("/about"));
assert_eq!(
Some(AppRoute::AboutWithSlash),
AppRoute::recognize("/about/")
);

// to_path preserves trailing slash from the route definition
assert_eq!("/about", AppRoute::AboutNoSlash.to_path());
assert_eq!("/about/", AppRoute::AboutWithSlash.to_path());
}

#[test]
fn router_root_not_affected() {
#[derive(Routable, Debug, Clone, PartialEq)]
enum AppRoute {
#[at("/")]
Home,
#[at("/other")]
Other,
}

assert_eq!(Some(AppRoute::Home), AppRoute::recognize("/"));
}
36 changes: 34 additions & 2 deletions website/docs/concepts/router.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,32 @@ unmatched.
For more information about the route syntax and how to bind parameters, check
out [matchit](https://docs.rs/matchit/0.9/matchit/).

### Trailing Slashes

Routes are sensitive to trailing slashes. `/about` and `/about/` are distinct
routes and will not match each other. This matters because browsers resolve
relative paths differently depending on the trailing slash:

| URL in address bar | Relative reference `./image.jpg` resolves to |
| ------------------ | -------------------------------------------- |
| `/about` | `/image.jpg` |
| `/about/` | `/about/image.jpg` |

If your component uses relative asset paths, make sure the route definition and
the links pointing to it agree on the trailing slash.

You can define both forms as separate variants if you need both URLs to work:

```rust ,ignore
#[derive(Clone, Routable, PartialEq)]
enum Route {
#[at("/about")]
About,
#[at("/about/")]
AboutSlash,
}
```

### Location

The router provides a universal `Location` struct via context which can be used to access routing information.
Expand Down Expand Up @@ -396,8 +422,11 @@ import ThemedImage from '@theme/ThemedImage'
}}
/>

The nested `SettingsRouter` handles all URLs that start with `/settings`. Additionally, it redirects URLs that are not
matched to the main `NotFound` route. So `/settings/gibberish` will redirect to `/404`.
The nested `SettingsRouter` handles all URLs that start with `/settings`. The outer router
defines a separate `SettingsSlash` variant for `/settings/` and redirects it to `SettingsRoot`,
because the inner `SettingsRoute` only defines routes without a bare trailing slash
(see [Trailing Slashes](#trailing-slashes)). Unrecognized sub-paths like `/settings/gibberish`
are redirected to the main `NotFound` route at `/404`.

:::caution

Expand All @@ -423,6 +452,8 @@ enum MainRoute {
Contact,
#[at("/settings")]
SettingsRoot,
#[at("/settings/")]
SettingsSlash,
#[at("/settings/{*_rest}")]
Settings { _rest: String },
#[not_found]
Expand All @@ -449,6 +480,7 @@ fn switch_main(route: MainRoute) -> Html {
MainRoute::News => html! {<h1>{"News"}</h1>},
MainRoute::Contact => html! {<h1>{"Contact"}</h1>},
MainRoute::SettingsRoot | MainRoute::Settings { .. } => html! { <Switch<SettingsRoute> render={switch_settings} /> },
MainRoute::SettingsSlash => html! { <Redirect<MainRoute> to={MainRoute::SettingsRoot}/> },
MainRoute::NotFound => html! {<h1>{"Not Found"}</h1>},
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,29 @@ enum Route {

ルーティング構文やパラメータのバインディング方法の詳細については、[matchit](https://docs.rs/matchit/0.9/matchit/) を参照してください。

### 末尾のスラッシュ

ルーティングでは末尾のスラッシュが区別されます。`/about` と `/about/` は異なるルートであり、互いにマッチしません。これはブラウザが末尾のスラッシュの有無に応じて相対パスを異なる方法で解決するため重要です:

| アドレスバーの URL | 相対参照 `./image.jpg` の解決先 |
| ------------------ | ------------------------------- |
| `/about` | `/image.jpg` |
| `/about/` | `/about/image.jpg` |

コンポーネントで相対アセットパスを使用する場合は、ルート定義とそれを指すリンクの末尾のスラッシュが一致していることを確認してください。

両方の URL を動作させたい場合は、両方の形式を別々のバリアントとして定義できます:

```rust ,ignore
#[derive(Clone, Routable, PartialEq)]
enum Route {
#[at("/about")]
About,
#[at("/about/")]
AboutSlash,
}
```

### 位置 (Location)

ルーターはコンテキストを介して一般的な `Location` 構造体を提供し、ルート情報にアクセスするために使用できます。これらはフックまたは `ctx.link()` 上の便利な関数を介して取得できます。
Expand Down Expand Up @@ -370,7 +393,7 @@ import ThemedImage from '@theme/ThemedImage'
}}
/>

ネストされた `SettingsRouter` は、すべての `/settings` で始まる URL を処理します。また、一致しない URL をメインの `NotFound` ルートにリダイレクトします。したがって、`/settings/gibberish` `/404` にリダイレクトされます。
ネストされた `SettingsRouter` は、すべての `/settings` で始まる URL を処理します。外側のルーターでは `/settings/` 用に別の `SettingsSlash` バリアントを定義し、`SettingsRoot` にリダイレクトします。これは、内側の `SettingsRoute` が末尾スラッシュのみのルートを定義していないためです([末尾のスラッシュ](#末尾のスラッシュ) を参照)。`/settings/gibberish` のような認識されないサブパスは、メインの `NotFound` ルート `/404` にリダイレクトされます。

:::caution

Expand All @@ -396,6 +419,8 @@ enum MainRoute {
Contact,
#[at("/settings")]
SettingsRoot,
#[at("/settings/")]
SettingsSlash,
#[at("/settings/{*_rest}")]
Settings { _rest: String },
#[not_found]
Expand All @@ -422,6 +447,7 @@ fn switch_main(route: MainRoute) -> Html {
MainRoute::News => html! {<h1>{"News"}</h1>},
MainRoute::Contact => html! {<h1>{"Contact"}</h1>},
MainRoute::SettingsRoot | MainRoute::Settings { .. } => html! { <Switch<SettingsRoute> render={switch_settings} /> },
MainRoute::SettingsSlash => html! { <Redirect<MainRoute> to={MainRoute::SettingsRoot}/> },
MainRoute::NotFound => html! {<h1>{"Not Found"}</h1>},
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,29 @@ enum Route {

有关路由语法和如何绑定参数的更多信息,请查看 [matchit](https://docs.rs/matchit/0.9/matchit/)。

### 尾部斜杠

路由区分尾部斜杠。`/about` 和 `/about/` 是不同的路由,不会互相匹配。这很重要,因为浏览器根据尾部斜杠的有无以不同的方式解析相对路径:

| 地址栏中的 URL | 相对引用 `./image.jpg` 解析为 |
| -------------- | ----------------------------- |
| `/about` | `/image.jpg` |
| `/about/` | `/about/image.jpg` |

如果您的组件使用相对资源路径,请确保路由定义和指向它的链接在尾部斜杠上保持一致。

如果您需要两种 URL 都能工作,可以将两种形式定义为不同的枚举变体:

```rust ,ignore
#[derive(Clone, Routable, PartialEq)]
enum Route {
#[at("/about")]
About,
#[at("/about/")]
AboutSlash,
}
```

### 位置 (Location)

路由器通过上下文提供了一个通用的 `Location` 结构,可以用于访问路由信息。它们可以通过钩子或 `ctx.link()` 上的便捷函数来检索。
Expand Down Expand Up @@ -370,7 +393,7 @@ import ThemedImage from '@theme/ThemedImage'
}}
/>

嵌套的 `SettingsRouter` 处理所有以 `/settings` 开头的 URL。此外,它会将未匹配的 URL 重定向到主 `NotFound` 路由。因此,`/settings/gibberish` 将重定向到 `/404`。
嵌套的 `SettingsRouter` 处理所有以 `/settings` 开头的 URL。外层路由器为 `/settings/` 定义了一个单独的 `SettingsSlash` 变体,并将其重定向到 `SettingsRoot`,因为内层 `SettingsRoute` 没有定义仅含尾部斜杠的路由(参见[尾部斜杠](#尾部斜杠))。无法识别的子路径(如 `/settings/gibberish`)会重定向到主 `NotFound` 路由 `/404`。

:::caution

Expand All @@ -396,6 +419,8 @@ enum MainRoute {
Contact,
#[at("/settings")]
SettingsRoot,
#[at("/settings/")]
SettingsSlash,
#[at("/settings/{*_rest}")]
Settings { _rest: String },
#[not_found]
Expand All @@ -422,6 +447,7 @@ fn switch_main(route: MainRoute) -> Html {
MainRoute::News => html! {<h1>{"News"}</h1>},
MainRoute::Contact => html! {<h1>{"Contact"}</h1>},
MainRoute::SettingsRoot | MainRoute::Settings { .. } => html! { <Switch<SettingsRoute> render={switch_settings} /> },
MainRoute::SettingsSlash => html! { <Redirect<MainRoute> to={MainRoute::SettingsRoot}/> },
MainRoute::NotFound => html! {<h1>{"Not Found"}</h1>},
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,29 @@ enum Route {

有關路由語法和如何綁定參數的更多信息,請查看 [matchit](https://docs.rs/matchit/0.9/matchit/)。

### 尾部斜線

路由會區分尾部斜線。`/about` 和 `/about/` 是不同的路由,不會互相匹配。這很重要,因為瀏覽器根據尾部斜線的有無以不同的方式解析相對路徑:

| 網址列中的 URL | 相對引用 `./image.jpg` 解析為 |
| -------------- | ----------------------------- |
| `/about` | `/image.jpg` |
| `/about/` | `/about/image.jpg` |

如果您的元件使用相對資源路徑,請確保路由定義和指向它的連結在尾部斜線上保持一致。

如果您需要兩種 URL 都能運作,可以將兩種形式定義為不同的列舉變體:

```rust ,ignore
#[derive(Clone, Routable, PartialEq)]
enum Route {
#[at("/about")]
About,
#[at("/about/")]
AboutSlash,
}
```

### 位置 (Location)

路由器透過上下文提供了一個通用的 `Location` 結構,可以用來存取路由資訊。它們可以透過鉤子或 `ctx.link()` 上的便捷函數來檢索。
Expand Down Expand Up @@ -370,7 +393,7 @@ import ThemedImage from '@theme/ThemedImage'
}}
/>

嵌套的 `SettingsRouter` 處理所有以 `/settings` 開頭的 URL。此外,它會將未符合的 URL 重新導向到主 `NotFound` 路由。因此,`/settings/gibberish` 將會重新導向到 `/404`。
嵌套的 `SettingsRouter` 處理所有以 `/settings` 開頭的 URL。外層路由器為 `/settings/` 定義了一個單獨的 `SettingsSlash` 變體,並將其重新導向到 `SettingsRoot`,因為內層 `SettingsRoute` 沒有定義僅含尾部斜線的路由(參見[尾部斜線](#尾部斜線))。無法識別的子路徑(如 `/settings/gibberish`)會重新導向到主 `NotFound` 路由 `/404`。

:::caution

Expand All @@ -396,6 +419,8 @@ enum MainRoute {
Contact,
#[at("/settings")]
SettingsRoot,
#[at("/settings/")]
SettingsSlash,
#[at("/settings/{*_rest}")]
Settings { _rest: String },
#[not_found]
Expand All @@ -422,6 +447,7 @@ fn switch_main(route: MainRoute) -> Html {
MainRoute::News => html! {<h1>{"News"}</h1>},
MainRoute::Contact => html! {<h1>{"Contact"}</h1>},
MainRoute::SettingsRoot | MainRoute::Settings { .. } => html! { <Switch<SettingsRoute> render={switch_settings} /> },
MainRoute::SettingsSlash => html! { <Redirect<MainRoute> to={MainRoute::SettingsRoot}/> },
MainRoute::NotFound => html! {<h1>{"Not Found"}</h1>},
}
}
Expand Down
Loading