React Router v4 & v5 攔截器(鉤子)、靜態路由、route-view 實現

前提

React Routerv3 版本之前 是有 onEnter 鉤子函數的,也支持靜態路由配置;,但到了 v4 版本後鉤子函數被移除,官方說是爲了將此提供給開發者,由開發者自由發揮。既然如此我們就只能自己實現,目前網上有很多版本,大多都是差不多的,這裏做一個總結並深化一下。同時提供鉤子函數或者vue中叫路由守衛和靜態化路由配置。

鉤子函數實現

鉤子函數實現比較簡單,只需要包裝一下官方的路由即可實現,這裏我們允許鉤子支持 Promise 異步,就需要一個異步組件;代碼如下

異步組件代碼

class AsyncBeforeEnter extends React.PureComponent {
    constructor(props) {
        super(props)
        this.state = {
            // 注意:此處component和render不會同時使用,同Route中component和render,render方法優先級要高
            // 目標組件 同 <Route componet>
            Component: null,
            // 目標組件render 方法  同 <Route render>
            render: null,
            // 錯誤信息
            error: null,
            // 標記異步是否完成
            completed: false
        }
    }
    componentDidMount() {
        const { beforeEnter, ...props } = this.props
        // beforeEnter 鉤子函數
        const enter = beforeEnter({ ...props })
        if (isPromise(enter)) {
            // 判斷是否是Promise
            enter
                .then(next => {
                    this.handleAfterEnter(next)
                })
                .catch(error => {
                    console.error(error)
                    this.setState({ error })
                })
        } else {
            this.handleAfterEnter(enter)
        }
    }
    handleAfterEnter(next) {
        // 結果處理
        const { route = {}, ...props } = this.props
        // 如果結果是null 或者undefined 或者 true : 不做任何處理直接渲染組件
        if (next === null || next === undefined || next === true) {
            this.completed(route.component, route.render)
            return
        }
        // 返回false:阻止組件的渲染
        if (next === false) {
            this.completed(null)
            return
        }

        // 返回 string : 跳轉的路由,類似http中302狀態碼
        // 這裏使用 React Router 的 Redirect 做跳轉
        if (typeof next === 'string') {
            this.completed(null, () => <Redirect to={next} from={props.location.pathname} />)
            return
        }
        // 返回React 組件
        if (typeof next === 'function' || React.isValidElement(next)) {
            this.completed(null, () => next)
            return
        }

        // 返回 Object: 如果有 redirect=true 的屬性,做跳轉
        // 否則使用 Route 組件渲染
        if (isPlainObject(next)) {
            const { redirect, ...nextProps } = next
            if (redirect === true) {
                this.completed(null, () => <Redirect {...nextProps} {...{ from: props.location.pathname }} />)
                return
            }
            this.completed(() => <Route {...nextProps} />)
            return
        }
        warn(`"${props.location.pathname} => beforeEnter"
hook return values error. expected null? undefined? true? React.Component? HTMLElement? Route props?
route props detail to see
https://reacttraining.com/react-router/web/api/Route
https://reacttraining.com/react-router/web/api/Redirect`
        )
        // 例外情況 阻止組件的渲染
        this.completed(null)
    }
    /**
     * 完成後改變state渲染組件:
     * @param component 
     * @param render 
     */
    completed(component, render) {
        this.setState({ Component: component, render, completed: true, error: null })
    }

    getExtraProps() {
        // 去掉鉤子函數,獲取其他props
        const { loading: Loading, beforeEnter, ...props } = this.props
        return { ...props }
    }
    render() {
        const { Component, render, error, completed } = this.state
        if (!completed) {
            // 未完成
            return null
        }
        // 已完成
        if (render && typeof render === 'function') {
            return render(this.getExtraProps())
        }
        return Component ? <Component {...this.getExtraProps()} /> : null
    }
}

帶有鉤子函數的 Route

將其命名爲 PrivateRoute

export default (route) => (
    <Route
        path={route.path}
        exact={route.exact}
        strict={route.strict}
        location={route.location}
        sensitive={route.sensitive}
        children={route.children}
        render={props => {
            // beforeEnter
            const { beforeEnter, ...nextProps } = route
            // 如果有鉤子函數,執行帶有異步組件
            if (route.beforeEnter && typeof route.beforeEnter === 'function') {
                return (
                    <AsyncBeforeEnter
                        beforeEnter={beforeEnter}
                        route={nextProps}
                        {...props}
                        {...extraProps}
                    />
                )

            }
            // 直接渲染
            return (
                route.render && typeof route.render ?
                    (
                        route.render({ ...props, ...extraProps, route: nextProps })
                    ) : (
                        route.component ? (
                            <route.component
                                route={nextProps}
                                {...props}
                                {...extraProps}
                            />
                        ) : null
                    )
            )
        }}
    />
)

使用的時候就可以用該 Route 代替官方的
示例:

<PrivateRoute path="/" component={Example} beforeEnter={(props) => check(props) }/>
<PrivateRoute path="/user" component={User} beforeEnter={(props) => check(props) }/>

靜態化路由配置,並支持鉤子函數

靜態化路由配置官方頁給出了方案,見:react-router-config,本文的靜態路由配置也是參考了該實現,並重寫了其中的實現,加入鉤子函數

靜態路由配置

基本的靜態路由表如下


// 頂級兩個路由
// 一個登錄
// 其他需要授權後放回
export default [
    {
        path: '/example',
        key: 'example',
        component: Example,
        beforeEnter(props) {
            if (auth(props.localtion.pathname)) {
                return true
            }
            return '/login'
        },
        // 子路由
        routes: [
            {
                path: '/example1',
                key: 'example1',
                component: Example1,
            }
        ]
    },
    {
        path: '/login',
        key: 'login',
        component: Login
    }
]

改寫鉤子函數使其能渲染靜態路由表=>renderRoutes


// renderRoutes
export default (routes, switchProps = {}, extraProps = {}) => {
    return routes && routes.length > 0 ? (
        <Switch {...switchProps}>
            {
                routes.map((route, i) => (
                    <Route
                        key={route.key || i}
                        path={route.path}
                        exact={route.exact}
                        strict={route.strict}
                        location={route.location}
                        sensitive={route.sensitive}
                        children={route.children}
                        render={props => {
                            // beforeEnter
                            const { beforeEnter, ...nextProps } = route
                            if (route.beforeEnter && typeof route.beforeEnter === 'function') {
                                return (
                                    <AsyncBeforeEnter
                                        beforeEnter={beforeEnter}
                                        route={nextProps}
                                        {...props}
                                        {...extraProps}
                                    />
                                )

                            }
                            return (
                                route.render && typeof route.render ?
                                    (
                                        route.render({ ...props, ...extraProps, route: nextProps })
                                    ) : (
                                        route.component ? (
                                            <route.component
                                                route={nextProps}
                                                {...props}
                                                {...extraProps}
                                            />
                                        ) : null
                                    )
                            )
                        }}
                    />
                ))
            }
        </Switch>
    ) : null
}

使用就可以調用 renderRoutes 方法 , 該實例摘自官方示例:

const Root = ({ route }) => (
  <div>
    <h1>Root</h1>
    {/* child routes won't render without this */}
    {renderRoutes(route.routes)}
  </div>
);

const Home = ({ route }) => (
  <div>
    <h2>Home</h2>
  </div>
);

const Child = ({ route }) => (
  <div>
    <h2>Child</h2>
    {/* child routes won't render without this */}
    {renderRoutes(route.routes, { someProp: "these extra props are optional" })}
  </div>
);

const GrandChild = ({ someProp }) => (
  <div>
    <h3>Grand Child</h3>
    <div>{someProp}</div>
  </div>
);

ReactDOM.render(
  <BrowserRouter>
    {/* kick it all off with the root route */}
    {renderRoutes(routes)}
  </BrowserRouter>,
  document.getElementById("root")
);

實現類似 vue-router 裏面的 route-view 功能

經過以上的處理,基本的鉤子函數和靜態路由就算配置完成了;功能雖然完成了,但總感覺使用上有點麻煩;確實,有沒有類似 vue-router 中的 route-view 這種的一步到位的呢?好的,安排。。。
這裏需要用到 React contextv16 以前這是不推薦的,不過現在已經成熟了,可以大膽的用了;如果不知道怎麼用和什麼原理可以 去這裏 補一下知識
在這裏插入圖片描述

這裏還有一個很關鍵的地方,看圖劃重點:
在這裏插入圖片描述

可以重複使用,內部的值會覆蓋外層的值,這樣我們就可以多層路由嵌套了;

創建 context

import React from 'react'

const RouteContext = React.createContext([])
// devtool 中使用
RouteContext.displayName = 'RouteViewContext'

export const RouteProvider = RouteContext.Provider
export const RouteConsumer = RouteContext.Consumer

創建 RouteView

import { RouteConsumer } from './context'
import renderRoutes from './renderRoutes'

//RouteView
export default () => {
    return (
        <RouteConsumer>
            {/* 使用靜態路由渲染 */}
            {/* ruotes 由RouteProvider 提供 */}
            {routes => renderRoutes(routes)}
        </RouteConsumer>
    )
}

再次改寫 renderRoutes, 使其能夠渲染下級路由

import { RouteProvider } from './context'

// renderRoutes
export default (routes, switchProps = {}, extraProps = {}) => {
    return routes && routes.length > 0 ? (
        <Switch {...switchProps}>
            {
                routes.map((route, i) => (
                    <Route
                        key={route.key || i}
                        path={route.path}
                        exact={route.exact}
                        strict={route.strict}
                        location={route.location}
                        sensitive={route.sensitive}
                        children={route.children}
                        render={props => {
                            checkProps(props)
                            // beforeEnter
                            const { beforeEnter, ...nextProps } = route

                            // RouteProvider 提供下級路由所需的數據
                            if (route.beforeEnter && typeof route.beforeEnter === 'function') {
                                return (
                                    <RouteProvider value={route.routes}>
                                        <AsyncBeforeEnter
                                            beforeEnter={beforeEnter}
                                            route={nextProps}
                                            {...props}
                                            {...extraProps}
                                        />
                                    </RouteProvider>
                                )

                            }
                            return (
                                <RouteProvider value={route.routes}>
                                    {
                                        route.render && typeof route.render ?
                                            (
                                                route.render({ ...props, ...extraProps, route: nextProps })
                                            ) : (
                                                route.component ? (
                                                    <route.component
                                                        route={nextProps}
                                                        {...props}
                                                        {...extraProps}
                                                    />
                                                ) : null
                                            )
                                    }
                                </RouteProvider>
                            )
                        }}
                    />
                ))
            }
        </Switch>
    ) : null
}

使用

在入口js添加代碼如下

import { RouteProvider, RouteView } from '../router'

// 靜態路由
const routes = [
    // 略。。。
]
class App extends React.PureComponent {
    // 略。。。
    render() {

        return (
            // 略。。。
            // 提供頂層路由即可
            // 下級路由 renderRoutes 處理
            <RouteProvider value={routes}>
                <RouteView />
            </RouteProvider>
            // 略。。。
        )
    }
}
export default App

二級路由使用

class Example extends React.PureComponent {
    // 略。。。
    render() {
        // 此處便不需要再提供routes了
        // 在 renderRoutes 已經由 RouteProvider 提供了
        return (
            <div>
                Example
                <RouteView />
            </div>
        )
    }
}
export default Example

通過以上努力,我們就具備了靜態路由、鉤子函數、類似 vue-routerrouter-view
最終的努力的結果:

參考文章

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章