您现在的位置是:网站首页> 内容页

React 系列 - 写出优雅的路由

  • 万象城官网
  • 2019-09-24
  • 274人已阅读
简介前言自前端框架风靡以来,路由一词在前端的热度与日俱增,他是几乎所有前端框架的核心功能点。不同于后端,前端的路由往往需要表达更多的业务功能,例如与菜单耦合、与标题耦合、与“面包屑”耦合等

前言

自前端框架风靡以来,路由一词在前端的热度与日俱增,他是几乎所有前端框架的核心功能点。不同于后端,前端的路由往往需要表达更多的业务功能,例如与菜单耦合、与标题耦合、与“面包屑”耦合等等,因此很少有拆箱即用的完整方案,多多少少得二次加工一下。

1. UmiJS 简述

优秀的框架可以缩短 90% 以上的无效开发时间,蚂蚁的 UmiJS 是我见过最优雅的 React 应用框架,或者可以直接说是最优雅的前端解决方案(欢迎挑战),本系列将逐步展开在其之上的应用,本文重点为“路由”,其余部分后续系列继续深入。

2. 需求概述

动码之前先构想下本次我们要实现哪些功能:

    路由需要耦合菜单,且需要对菜单的空节点自动往下补齐;路由中总要体现模板的概念,即不同的路由允许使用不用的模板组件;模板与页面的关系完全交由路由组合,不再体现于组件中;需要实现从路由中获取当前页面的轨迹,即“面包屑”的功能;实现从路由中获取页面标题;

上述每一点的功能都不复杂,若不追求极致,其实默认的约定式路由基本能够满足需求(详情查询官方文档,此处不做展开)。

3. 开码

3.1 菜单

先从菜单出发,以下应当是一个最简洁的目录结构:

const menu = [ { name: "父节点", path: "parent", children: [{ name: "子页面", path: "child" }] }];

使用递归补齐 child 路径:

const reg = /(((^https?:(?://)?)(?:[-;:&=+$,w]+@)?[A-Za-z0-9.-]+(?::d+)?|(?:www.|[-;:&=+$,w]+@)[A-Za-z0-9.-]+)((?:/[+~%/.w-_]*)???(?:[-+=&;%@.w_]*)#?(?:[w]*))?)$/;const formatMenu = (data, parentPath = `${define.BASE_PATH}/`) => { return data.map((item) => { let { path } = item; if (!reg.test(path)) { path = parentPath + item.path; } const result = { ...item, path }; if (item.children) { result.children = formatMenu(item.children, `${parentPath}${item.path}/`); } return result; });}

菜单的子节点才是真正的页面,所以若当前路径是父节点,我们期望的是能够自动跳转到父节点写的第一个或者特定的页面:

const redirectData = [];const formatRedirect = item => { if (item && item.children) { if (item.children[0] && item.children[0].path) { redirectData.push({ path: `${item.path}`, redirect: `${item.children[0].path}` }); item.children.forEach(children => { formatRedirect(children); }); } }};const getRedirectData = (menuData) => { menuData.forEach(formatRedirect); return redirectData};

3.2 路由组装

而后便是将自动跳转的路径组装入路由节点:

const routes = [ ...redirect, { path: define.BASE_PATH, component: "../layouts/BasicLayout", routes: [ { path: `${define.BASE_PATH}/parent`, routes: [ { title: "子页面", path: "child", component: "./parent/child", } ], }, { component: "./404", } ] }];

路由配置最后需要注入配置文件 .umirc.js:

import { plugins } from "./config/plugins";import { routes } from "./config/routes";export default { plugins, routes}

3.3 模板页

import { Layout } from "antd";import React, { PureComponent, Fragment } from "react";import { ContainerQuery } from "react-container-query";import DocumentTitle from "react-document-title";import { query } from "@/utils/layout";import Footer from "./Footer";import Context from "./MenuContext";const { Content } = Layout;class BasicLayout extends PureComponent { render() { const { children, location: { pathname } } = this.props; const layout = ( <Layout> <Layout> <Content> {children} </Content> <Footer /> </Layout> </Layout> ); return ( <Fragment> <DocumentTitle title={this.getPageTitle(pathname)}> <ContainerQuery query={query}> {params => ( <Context.Provider> {layout} </Context.Provider> )} </ContainerQuery> </DocumentTitle> </Fragment> ); }}export default BasicLayout;

结合路由与菜单获取面包屑:

getBreadcrumbNameMap() { const routerMap = {}; let path = this.props.location.pathname; if (path.endsWith("/")) { path = path.slice(0, path.length - 1); } const mergeRoute = (path) => { if (path.lastIndexOf("/") > 0) { const title = this.getPageTitle(path); if (title) { routerMap[path] = { name: title, path: path }; } mergeRoute(path.slice(0, path.lastIndexOf("/"))); } }; const mergeMenu = data => { data.forEach(menuItem => { if (menuItem.children) { mergeMenu(menuItem.children); } routerMap[menuItem.path] = { isMenu: true, ...menuItem }; }); }; mergeRoute(path); mergeMenu(this.state.menuData); return routerMap;}

从路由中获取 PageTitle:

getPageTitle = (path) => { if (path.endsWith("/")) { path = path.slice(0, path.length - 1); } let title; this.props.route.routes[0].routes.forEach(route => { if (route.path === path) { title = route.title; return; } }) return title;};

结语

此篇随笔比较混乱,写作脉络不对,还是应该简述下在 umijs 之上的架构设计,再往下深入探讨应用点,缺的部分会在后续系列中补上~ 请关注公众号:

文章评论

Top