5分钟搞定react服务端路由

237 阅读2分钟

什么是服务端路由?

平时我们项目的路由配置可能是这样的:

export const router: RoutersConfig[] = [
    {
        path: '/',
        name: 'app',
        element: <BasicLayout />,
        children: [
            {
                path: '/', 
                name: 'dashboard', 
                element: <Dashboard />,
                index: true
            }, {
                path: 'login2',
                name: 'login2', 
                element: <Login/>,
                free: true,
                children: [
                   {
                        path: 'async2',
                        name: 'async2', 
                        element: <Login />,
                        free: true
                    }
                ]
            }, {
                path: '/async',
                name: 'async', 
                element: <Login />,
                free: true
            }
        ]
    }, {
        path: '/login',
        name: 'login',
        element: <Login />,
    }

]

export function dispatchRouter(config: RoutersConfig[], parentPath = '') {
    return config.map(item => {

        let props = {
            ...item,
            children: null,
        };
      
        if (parentPath) {
            props.path = `${parentPath}${item.path}`;
        }

        return <Route {...props} key={props.path}>
            { item.children && dispatchRouter(item.children, props.path) }
        </Route>
    })
}

将所有的路由单独放到一个文件里。这也是最方便的一种方式。然后通过dispatchRouter递归route tree。最终生成这样的结构:

image.png

如果我们要求路由根据用户的权限去生成,这种方式就不能满足。那么我们就需要服务端路由。简单的说就是:route tree不再是前端定义, 而是根据后端返回的数据去生成。

但是有一个问题就是,后端和我交互的数据一般都是json格式,组件是不能被后端返回的,除非使用SSR。关于SSR的内容暂时不做赘述。

所以只能让后端返回组件路径,我们动态的导入从而生成各个<Route />。所以服务端路由主要就问题就是解决动态导入的问题。下面通过代码一步一步的解决这个问题。

Coding time

  1. 我们首先搭建一个react项目,不限vite或者webpack, 这里我使用的是vite
  2. 并安装上react-router-dom或者react-router
  3. 开始撸代码

项目结构设计

首先我们确定有两个通用路由,分别是登录路由和root路由。所以这两个路由我们仍然在前端提前定义好

        <BrowserRouter>
            <Routes>
                <Route path="/" element={<SecurityLayout />}>
                    <Route path="*" element={<div>405</div>} />
                </Route>
                <Route path="/login" element={<Login />}>
                    <Route path="/login/register" element={<div>注册</div>} />
                </Route>
                <Route path="*" element={<div>404</div>} />
            </Routes>
        </BrowserRouter>

从上面代码中可以看出我们定义了一个404路由,一个根路由和一个登录路由。

我们需要在请求路由的时候做个等待效果

const App = () => {
    const [loading, setLoading] = useState(true);
    if (loading) {
        return <div>加载中....</div>;
    }
    return (
        <BrowserRouter>
            <Routes>
                <Route path="/" element={<SecurityLayout />}>
                    <Route path="*" element={<div>405</div>} />
                </Route>
                <Route path="/login" element={<Login />}>
                    <Route path="/login/register" element={<div>注册</div>} />
                </Route>
                <Route path="*" element={<div>404</div>} />
            </Routes>
        </BrowserRouter>
    );
};

效果就出来了

image.png

请求路由数据

我们用一个timeout来模拟api请求

const mockRoutes = () => {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve([
                {
                    path: '/',
                    name: 'dashboard',
                    component: '/dashboard/index',
                    index: true,
                },
                {
                    path: '/requestList',
                    name: 'dashboard1',
                    component: '/dashboard/index',
                    // index: true
                },
                {
                    path: '/userList',
                    name: 'userList',
                    component: '/userList/index',
                }
            ]);
        }, 3000);
    });
};

然后在useEffect内请求数据

    const fetechRoutes = async () => {
        const serverSideRoutes = (await mockRoutes()) as any[];
    };
    useEffect(() => {
        fetechRoutes();
    }, []);

递归生成路由并保持到state内

const modules = import.meta.glob('./pages/*/*.tsx');

const pollRoutes = (items: any[], parentPath = '') => {
        return items.map((item) => {
            const props = {
                ...item,
                children: null
            };
            if (item.component) {
                // 动态导入组件
                const Component = modules[`./pages${item.component}.tsx`];
                const MyCom =  lazy(Component as any);
                props.element = <MyCom />;
            }
            if (parentPath) {
                props.path = `${parentPath}${item.path}`;
            }

            return (
                // eslint-disable-next-line react/jsx-props-no-spreading
                <Route {...props} key={props.path}>
                    {item.children && dispatchRouter(item.children, props.path)}
                </Route>
            );
        });
    };
    const fetechRoutes = async () => {
        const serverSideRoutes = (await mockRoutes()) as any[];
        const routerTree = pollRoutes(serverSideRoutes);
        setRoutes(routerTree);
        setLoading(false);
    };
    useEffect(() => {
        fetechRoutes();
    }, []);

vite不支持 import($url)的这种写法, 但是提供import.meta.glob所以如果项目使用的是vite就不能直接用import 如果是webpack 只需要以下操作即可

 if (item.component) {
    // 动态导入组件
    const Component = modules[`./pages${item.component}.tsx`];
    const MyCom =  lazy(Component as any);
    props.element = <MyCom />;
}
/* 改成 */
 if (item.component) {
    // 动态导入组件
    const MyCom =  lazy(() => import(`${url}`));
    props.element = MyCom;
}