如何用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
相关产品推荐
相关产品推荐

