React 系列 - 寫出優雅的路由

前言

自前端框架風靡以來,路由一詞在前端的熱度與日俱增,他是幾乎全部前端框架的核心功能點。不一樣於後端,前端的路由每每須要表達更多的業務功能,例如與菜單耦合、與標題耦合、與「麪包屑」耦合等等,所以不多有拆箱即用的完整方案,多多少少得二次加工一下。
前端

1. UmiJS 簡述

優秀的框架能夠縮短 90% 以上的無效開發時間,螞蟻的 UmiJS 是我見過最優雅的 React 應用框架,或者能夠直接說是最優雅的前端解決方案(歡迎挑戰),本系列將逐步展開在其之上的應用,本文重點爲「路由」,其他部分後續系列繼續深刻。react

2. 需求概述

動碼以前先構想下本次咱們要實現哪些功能:後端

  1. 路由須要耦合菜單,且須要對菜單的空節點自動往下補齊;
  2. 路由中總要體現模板的概念,即不一樣的路由容許使用不用的模板組件;
  3. 模板與頁面的關係徹底交由路由組合,再也不體現於組件中;
  4. 須要實現從路由中獲取當前頁面的軌跡,即「麪包屑」的功能;
  5. 實現從路由中獲取頁面標題;

上述每一點的功能都不復雜,若不追求極致,其實默認的約定式路由基本可以知足需求(詳情查詢官方文檔,此處不作展開)。前端框架

3. 開碼

3.1 菜單

先從菜單出發,如下應當是一個最簡潔的目錄結構:antd

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 路由組裝

然後即是將自動跳轉的路徑組裝入路由節點:ide

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:this

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;

結合路由與菜單獲取麪包屑:spa

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 之上的架構設計,再往下深刻探討應用點,缺的部分會在後續系列中補上~


個人公衆號《捷義》
qrcode_for_gh_c1a4cd5ae0fe_430.jpg

相關文章
相關標籤/搜索