前言
自前端框架風靡以來,路由一詞在前端的熱度與日俱增,他是幾乎所有前端框架的核心功能點。不同於后端,前端的路由往往需要表達更多的業務功能,例如與菜單耦合、與標題耦合、與“面包屑”耦合等等,因此很少有拆箱即用的完整方案,多多少少得二次加工一下。
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 之上的架構設計,再往下深入探討應用點,缺的部分會在后續系列中補上~ 請關注公眾號: