You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何用Fable.Feliz开发含路由/主题/多语言/授权的中型SPA?

基于Fable.Feliz的中型React应用实现方案

1. Props组合React组件

Fable.Feliz中组件Props以记录类型为核心,通过扩展记录或通用Props复用实现灵活组合:

基础组件封装(支持扩展HTML属性)

type ButtonProps = {
    Text: string
    OnClick: unit -> unit
    HtmlProps: Feliz.HTMLAttributes.button option
}

let Button (props: ButtonProps) =
    Html.button [
        yield! props.HtmlProps |> Option.defaultValue []
        prop.onClick props.OnClick
        prop.text props.Text
    ]

高阶组件增强(比如添加Loading状态)

let withLoading (isLoading: bool) (component: ReactElement) =
    if isLoading then
        Html.div [ prop.text "加载中..." ]
    else
        component

使用示例:

Button {
    Text = "提交"
    OnClick = fun () -> printfn "触发提交"
    HtmlProps = Some [ prop.style [ style.backgroundColor.blue ] ]
} |> withLoading false

2. 路由与母版页(布局/菜单)

基于Feliz.Router实现路由管理,母版页作为容器嵌套页面组件:

定义路由类型与解析逻辑

type Route =
    | Home
    | Products
    | ProductDetail of id: int
    | Profile
    | Login
    | NotFound

let parseUrl (url: string) =
    Router.parseUrl url [
        Router.route "" Home
        Router.route "products" Products
        Router.route "products/{id}" ProductDetail
        Router.route "profile" Profile
        Router.route "login" Login
        Router.route "*" NotFound
    ]

母版页布局(含导航菜单)

let Layout (currentRoute: Route) (content: ReactElement) =
    let _, _, langRes = React.useContext(LangContext)
    let theme, _ = React.useContext(ThemeContext)
    
    let navItemStyle route =
        [ if currentRoute = route then style.color.blue else style.color.black ]
    
    Html.div [
        prop.style (match theme with Light -> [ style.bgColor.white; style.color.black ] | Dark -> [ style.bgColor "#1a1a1a"; style.color.white ])
        // 顶部导航
        Html.nav [
            prop.style [ style.display.flex; style.gap 15; style.padding 10; style.borderBottom 1 solid gray ]
            Router.link (Router.format Route.Home) [ prop.text langRes.Home; prop.style (navItemStyle Home) ]
            Router.link (Router.format Route.Products) [ prop.text langRes.Products; prop.style (navItemStyle Products) ]
            Router.link (Router.format Route.Profile) [ prop.text langRes.Profile; prop.style (navItemStyle Profile) ]
            Html.div [ prop.style [ style.marginLeft "auto" ] ]
        ]
        // 页面内容区域
        Html.main [ prop.style [ style.padding 20 ]; prop.children content ]
    ]

主应用路由渲染

let App =
    React.functionComponent(fun () ->
        let currentRoute, setCurrentRoute = React.useState(Home)
        React.useEffectOnce(fun () -> Router.onUrlChanged (parseUrl >> setCurrentRoute))

        let pageContent =
            match currentRoute with
            | Home -> Html.h1 "首页内容"
            | Products -> Html.h1 "产品列表页面"
            | ProductDetail id -> Html.h1 (sprintf "产品详情:%d" id)
            | Profile -> withAuth None (fun _ -> Html.h1 "个人中心页面")
            | Login -> LoginPage
            | NotFound -> Html.h1 "404页面"

        AuthProvider (LangProvider (ThemeProvider (Layout currentRoute pageContent)))
    )

3. 深色/浅色主题切换

通过React Context管理主题状态,结合样式切换实现:

主题Context与提供者

type Theme = Light | Dark
let ThemeContext = React.createContext<Theme * (Theme -> unit)>(Light, fun _ -> ())

let ThemeProvider (children: ReactElement) =
    let theme, setTheme = React.useState(Light)
    React.contextProvider ThemeContext (theme, setTheme) children

主题切换按钮(嵌入布局)

在Layout组件中添加切换逻辑:

let toggleTheme () = setTheme (if theme = Light then Dark else Light)
Html.button [
    prop.text (if theme = Light then "切换深色" else "切换浅色")
    prop.onClick toggleTheme
]

4. 多语言支持

用React Context管理语言状态,通过资源字典映射文本:

语言类型与资源定义

type Language = ZhCN | EnUS
type LangResources = { Home: string; Products: string; Profile: string; Submit: string }

let getResources (lang: Language) =
    match lang with
    | ZhCN -> { Home = "首页"; Products = "产品列表"; Profile = "个人中心"; Submit = "提交" }
    | EnUS -> { Home = "Home"; Products = "Products"; Profile = "Profile"; Submit = "Submit" }

语言Context与提供者

let LangContext = React.createContext<Language * (Language -> unit) * LangResources>(ZhCN, fun _ -> (), getResources ZhCN)

let LangProvider (children: ReactElement) =
    let lang, setLang = React.useState(ZhCN)
    let resources = getResources lang
    React.contextProvider LangContext (lang, setLang, resources) children

语言切换按钮(嵌入布局)

let toggleLang () = setLang (if lang = ZhCN then EnUS else ZhCN)
Html.button [ prop.text "中文/English"; prop.onClick toggleLang ]

5. 授权功能(结合C# WebAPI)

通过React Context管理用户状态,封装授权高阶组件保护页面:

用户状态与Auth Context

type User = { Id: int; Username: string; Roles: string list } option
let AuthContext = React.createContext<User * (User -> unit)>(None, fun _ -> ())

Auth提供者(含登录/登出逻辑)

let AuthProvider (children: ReactElement) =
    let user, setUser = React.useState<User>(None)
    
    let login (username: string, password: string) =
        async {
            let! response = Fetch.post("/api/auth/login", {| Username = username; Password = password |})
            if response.Ok then
                let! userInfo = response.Json<User>()
                setUser (Some userInfo)
                let! token = response.Headers.get("Authorization")
                Browser.Dom.window.localStorage.setItem("authToken", token)
        } |> Async.StartImmediate
    
    let logout () =
        setUser None
        Browser.Dom.window.localStorage.removeItem("authToken")
    
    // 初始化时验证token
    React.useEffectOnce(fun () ->
        let token = Browser.Dom.window.localStorage.getItem("authToken")
        if token <> null then
            async {
                let! response = Fetch.get("/api/auth/me", [ Fetch.headers [ "Authorization", token ] ])
                if not response.Ok then logout ()
                else let! userInfo = response.Json<User>() in setUser (Some userInfo)
            } |> Async.StartImmediate
    )
    
    React.contextProvider AuthContext (user, setUser) children

授权高阶组件

let withAuth (requiredRoles: string list option) (component: User -> ReactElement) =
    React.functionComponent(fun () ->
        let user, _ = React.useContext(AuthContext)
        match user with
        | None ->
            React.useEffectOnce(fun () -> Router.navigate "/login")
            Html.div "请先登录"
        | Some u ->
            match requiredRoles with
            | Some roles when not (roles |> List.exists u.Roles.Contains) -> Html.div "无访问权限"
            | _ -> component u
    )

登录页面组件

let LoginPage =
    React.functionComponent(fun () ->
        let _, setUser = React.useContext(AuthContext)
        let username, setUsername = React.useState("")
        let password, setPassword = React.useState("")
        
        let handleLogin () = login (username, password)
        
        Html.div [
            Html.input [ prop.type'.text; prop.value username; prop.onChange (fun e -> setUsername e.Value) ]
            Html.input [ prop.type'.password; prop.value password; prop.onChange (fun e -> setPassword e.Value) ]
            Html.button [ prop.text "登录"; prop.onClick handleLogin ]
        ]
    )

内容的提问来源于stack exchange,提问作者user23655924

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.22 15:54:52