react-router-manage
Version:
react-router-manage extends react-router(v6), it Including authentication, configuration, addition, deletion and modification
1,955 lines (1,934 loc) • 52.7 kB
JavaScript
/**
* React Router Manage v2.0.4
*
* Copyright (c) onshinpei Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE.md file in the root directory of this source tree.
*
* @license MIT
*/
import {
useNavigate as useNavigate$1,
generatePath,
useLocation,
useParams,
Navigate as Navigate$1,
matchPath
} from "react-router";
export {
AbortedDeferredError,
Await,
MemoryRouter,
Navigate,
NavigationType,
Outlet,
Route,
Router,
RouterProvider,
Routes,
UNSAFE_DataRouterContext,
UNSAFE_DataRouterStateContext,
UNSAFE_LocationContext,
UNSAFE_NavigationContext,
UNSAFE_RouteContext,
createMemoryRouter,
createPath,
createRoutesFromChildren,
createRoutesFromElements,
defer,
generatePath,
isRouteErrorResponse,
json,
matchPath,
matchRoutes,
parsePath,
redirect,
renderMatches,
resolvePath,
unstable_useBlocker,
useActionData,
useAsyncError,
useAsyncValue,
useHref,
useInRouterContext,
useLoaderData,
useLocation,
useMatch,
useMatches,
useNavigation,
useNavigationType,
useOutlet,
useOutletContext,
useParams,
useResolvedPath,
useRevalidator,
useRouteError,
useRouteLoaderData,
useRoutes
} from "react-router";
import {
Router,
Navigate,
useLocation as useLocation$1,
useRoutes
} from "react-router-dom";
export {
Form,
Link,
NavLink,
ScrollRestoration,
UNSAFE_useScrollRestoration,
createBrowserRouter,
createHashRouter,
useBeforeUnload,
useFetcher,
useFetchers,
useFormAction,
useLinkClickHandler,
useSearchParams,
useSubmit
} from "react-router-dom";
import * as React from "react";
import {
useMemo,
useState,
useLayoutEffect,
useRef,
useCallback,
Suspense,
useReducer,
useImperativeHandle,
useEffect
} from "react";
import { unstable_batchedUpdates } from "react-dom";
import { createBrowserHistory, createHashHistory } from "@remix-run/router";
import { stringify, parse } from "query-string";
const MRouterHistoryContext = /*#__PURE__*/ React.createContext(null);
MRouterHistoryContext.displayName = "MRouterHistoryContext";
function useHistory() {
return React.useContext(MRouterHistoryContext).history;
}
function useRouteHooksRef() {
return React.useContext(MRouterHistoryContext).routeHooksRef;
}
function useHistoryMethods() {
return React.useContext(MRouterHistoryContext).historyMethods;
}
/**
* A `<Router>` for use in web browsers. Provides the cleanest URLs.
*/
function BrowserRouter({ basename, children, syncUpdateCurrentRoute }) {
const historyRef = React.useRef(null);
const routeHooksRef = React.useRef(null);
if (historyRef.current == null) {
const history = createBrowserHistory({
window,
v5Compat: true
});
historyRef.current = {
...history,
back: () => history.go(-1),
forward: () => history.go(1)
};
routeHooksRef.current = [];
}
const historyContext = useMemo(() => {
return {
history: historyRef.current,
routeHooks: routeHooksRef.current,
routeHooksRef,
historyMethods: {
push: historyRef.current.push,
replace: historyRef.current.replace,
go: historyRef.current.go,
back: historyRef.current.back,
forward: historyRef.current.forward
}
};
}, []);
const history = historyRef.current;
const [state, setState] = React.useState({
action: history.action,
location: history.location
});
React.useLayoutEffect(() => {
let mounted = true;
const removeListenFn = history.listen(routeData => {
const { location } = routeData;
if (!mounted) {
return;
}
unstable_batchedUpdates(() => {
setState(routeData);
syncUpdateCurrentRoute(location);
});
});
return () => {
mounted = false;
removeListenFn();
};
}, [history, syncUpdateCurrentRoute]);
return /*#__PURE__*/ React.createElement(
MRouterHistoryContext.Provider,
{
value: historyContext
},
/*#__PURE__*/ React.createElement(
Router,
{
basename: basename,
location: state.location,
navigationType: state.action,
navigator: history
},
children
)
);
}
/**
* A `<Router>` for use in web browsers. Provides the cleanest URLs.
*/
function HashRouter({ basename, children, syncUpdateCurrentRoute }) {
const historyRef = React.useRef(null);
const routeHooksRef = React.useRef(null);
if (historyRef.current == null) {
const history = createHashHistory({
window,
v5Compat: true
});
historyRef.current = {
...history,
back: () => history.go(-1),
forward: () => history.go(1)
};
routeHooksRef.current = [];
}
const historyContext = useMemo(() => {
return {
history: historyRef.current,
routeHooks: routeHooksRef.current,
routeHooksRef,
historyMethods: {
push: historyRef.current.push,
replace: historyRef.current.replace,
go: historyRef.current.go
}
};
}, []);
const history = historyRef.current;
const [state, setState] = React.useState({
action: history.action,
location: history.location
});
React.useLayoutEffect(() => {
let mounted = true;
const removeListenFn = history.listen(routeData => {
const { location } = routeData;
if (!mounted) {
return;
}
unstable_batchedUpdates(() => {
setState(routeData);
syncUpdateCurrentRoute(location);
});
});
return () => {
mounted = false;
removeListenFn();
};
}, [history, syncUpdateCurrentRoute]);
return /*#__PURE__*/ React.createElement(
MRouterHistoryContext.Provider,
{
value: historyContext
},
/*#__PURE__*/ React.createElement(
Router,
{
basename: basename,
location: state.location,
navigationType: state.action,
navigator: history
},
children
)
);
}
const PageConfig = {
"401": {
title: "401 NO PERMISSION",
img: "//ysf.qiyukf.net/yx/9c9ce7793b3c0657da5d80e740a89681"
},
"404": {
title: "404 NOT FOUND",
img: "https://ysf.nosdn.127.net/ysh/6be90dea7806767fe65e7b48982b7a61"
}
};
const NoAuth = ({ code = "401" }) => {
const config = PageConfig[code];
return /*#__PURE__*/ React.createElement(
"div",
{
style: {
display: "flex",
justifyContent: "center",
alignItems: "center",
background: "white",
height: "100%",
flexDirection: "column",
borderRadius: 8
}
},
/*#__PURE__*/ React.createElement("img", {
alt: config.title,
width: 200,
src: config?.img
}),
/*#__PURE__*/ React.createElement(
"div",
{
style: {
color: "#666666",
lineHeight: "22px",
textAlign: "center",
marginTop: 16
}
},
config.title
)
);
};
NoAuth.displayName = "NoAuth";
const NameRedirect = ({ name, component: Component }) => {
const { routesMap, currentRoute } = useRouter();
const targetRoute = routesMap[name];
if (!targetRoute) {
if (process.env.NODE_ENV !== "production");
}
if (name === currentRoute.name) {
if (Component) {
return /*#__PURE__*/ React.createElement(Component, null);
}
return /*#__PURE__*/ React.createElement(React.Fragment, null);
}
return /*#__PURE__*/ React.createElement(Navigate, {
to: targetRoute.path || ""
});
};
const Spin = ({ tip = "加载中" }) => {
return /*#__PURE__*/ React.createElement("div", null, tip);
};
const LoadingCmp = () =>
/*#__PURE__*/ React.createElement(
React.Fragment,
null,
/*#__PURE__*/ React.createElement(Spin, {
tip: "\u5E94\u7528\u6B63\u5728\u52A0\u8F7D\u4E2D\u2026"
})
);
const changeable = {
LoadingComponent: LoadingCmp
};
function setChangeable(options) {
Object.entries(options).forEach(([key, value]) => {
changeable[key] = value;
});
}
function getComponent(options, Component) {
let ReplaceComponent;
if (!options) {
return ReplaceComponent;
}
// if a component is passed in
if (isComponent(options)) {
ReplaceComponent = options;
// @ts-ignore
} else if (isString(options.path)) {
// if there is path, path is preferred
ReplaceComponent = function Redirect() {
// @ts-ignore
return /*#__PURE__*/ React.createElement(Navigate, {
to: options.path
});
};
// @ts-ignore
} else if (isString(options.name)) {
// if there is no path, use name
ReplaceComponent = function Redirect() {
// @ts-ignore
return /*#__PURE__*/ React.createElement(NameRedirect, {
name: options.name,
component: Component
});
};
}
return ReplaceComponent;
}
const GeneratorHookCom = ({ beforeEnter, Component, beforeEachMount }) => {
/**
* since setCurrentComponent(Component) Component may be a function
* react by default, if the preState is a function, the function will be executed and an error will occur
* So here we put Component into an object
*/
const [CurrentComponent, setCurrentComponent] = useState({
Component: undefined
});
const { currentRoute } = useRouter();
useLayoutEffect(() => {
// 是否激活状态(未卸载)
let isActive = true;
// 全局的
if (beforeEachMount) {
beforeEachMount(currentRoute, options => {
if (!isActive) {
return;
}
// global
const EachReplaceComponent = getComponent(options, Component);
if (beforeEnter) {
// local
beforeEnter(currentRoute, enterOptions => {
if (!isActive) {
return;
}
const EnterReplaceComponent = getComponent(
enterOptions,
EachReplaceComponent || Component
);
// if the Component is passed in next in beforeEnter, the beforeEnter shall prevail
// Otherwise, beforeEachBeforeMount shall prevail
setCurrentComponent({
Component:
EnterReplaceComponent || EachReplaceComponent || Component
});
});
} else {
setCurrentComponent({
Component: EachReplaceComponent || Component
});
}
});
} else {
// local
if (beforeEnter) {
beforeEnter(currentRoute, enterOptions => {
if (!isActive) {
return;
}
const EnterReplaceComponent = getComponent(enterOptions, Component);
setCurrentComponent({
Component: EnterReplaceComponent || Component
});
});
}
}
return () => {
isActive = false;
};
}, [Component, currentRoute, beforeEnter, beforeEachMount]);
const LoadingCmp = changeable.LoadingComponent;
return CurrentComponent.Component
? /*#__PURE__*/ React.createElement(CurrentComponent.Component, null)
: /*#__PURE__*/ React.createElement(LoadingCmp, null);
};
const NotFound = () => {
return /*#__PURE__*/ React.createElement(NoAuth, {
code: "404"
});
};
function useBeforeLeave(fn, options) {
const pathname = window.location.pathname;
const routeHooksRef = useRouteHooksRef();
const beforeunload = options?.beforeunload;
const beforeunloadRef = useRef(beforeunload);
beforeunloadRef.current = beforeunload;
useLayoutEffect(() => {
const hooks = routeHooksRef.current;
const routeHook = {
name: "BeforeRouterLeave",
pathname,
fn
};
hooks.push(routeHook);
// bind beforeunload
const currentBeforeunload = beforeunloadRef.current;
const beforeunloadFn = event => {
return currentBeforeunload?.(event);
};
if (currentBeforeunload) {
window.addEventListener("beforeunload", beforeunloadFn);
}
return () => {
const index = hooks.indexOf(routeHook);
hooks.splice(index, 1);
// unbind beforeunload
if (currentBeforeunload) {
window.removeEventListener("beforeunload", beforeunloadFn);
}
};
}, [fn, pathname, routeHooksRef]);
}
const useNavigate = () => {
const oldNavigate = useNavigate$1();
const newCallback = useCallback(
(to, options = {}) => {
if (options?.params && typeof to === "string") {
to = generatePath(to, options.params);
}
// query写入地址栏
if (options?.query && typeof to === "string") {
let path = to;
const queryStr = stringify(options.query);
if (path.includes("?")) {
path = `${path}&${queryStr}`;
} else {
path = `${path}?${queryStr}`;
}
to = path;
}
return oldNavigate(to, options);
},
[oldNavigate]
);
return newCallback;
};
function useRouter() {
const location = useLocation();
const routesMapRef = useRef({});
const {
routesMap,
inputRoutes,
currentRoute,
flattenRoutes,
authInputRoutes,
basename
} = useRouterState();
if (routesMapRef.current !== routesMap) {
routesMapRef.current = routesMap;
}
const search = useMemo(() => {
return parse(location.search);
}, [location.search]);
const routerParams = useParams();
const newNavigate = useNavigate();
return {
navigate: newNavigate,
routesMap,
query: search,
params: routerParams,
routes: inputRoutes,
authRoutes: authInputRoutes,
currentRoute,
flattenRoutes,
location,
basename
};
}
const RedirectChild = () => {
const { currentRoute } = useRouter();
const redirectPath = useMemo(() => {
if (!currentRoute) {
return "";
}
const itemsAndChildren = currentRoute._itemsAndChildren || [];
const childRoute = itemsAndChildren?.find(i => {
return i._isHasAuth;
});
return childRoute?.path || "";
}, [currentRoute]);
let replace = false;
// 父级也没配置component,则会进行多次重定向进行replace, 以便浏览器回退行为
if (!currentRoute.parent?.component) {
replace = true;
}
if (!redirectPath) {
return /*#__PURE__*/ React.createElement(NoAuth, null);
}
return /*#__PURE__*/ React.createElement(Navigate$1, {
to: redirectPath,
replace: replace
});
};
function proxyRoutesMapFromTarget(obj, target, filterKeys = []) {
const keys = Object.keys(target).filter(i => !filterKeys.includes(i));
keys.forEach(key => {
Object.defineProperty(obj, key, {
get: () => {
const route = target[key];
if (Array.isArray(route)) {
return route[route.length - 1];
}
return route;
}
});
});
}
function getWholePath(path = "", basename = "/", parentPath) {
if (path.startsWith(basename)) {
return path;
}
// 根路径
if (path === basename) {
return basename;
}
if (path.startsWith("/")) {
if (basename.endsWith("/")) {
return `${basename}${path.slice(1)}`;
}
return `${basename}${path}`;
}
if (parentPath) {
return getWholePath(path, parentPath);
}
if (basename.endsWith("/")) {
return `${basename}${path}`;
}
return `${basename}/${path}`;
}
function cloneRoutes(_routeConfig) {
const { routes, parent, basename = "/", _level = 1 } = _routeConfig;
if (!routes) {
return [];
}
function _cloneRoutes(_routes, parent, _level = 1) {
return _routes.map(_route => {
const { path, items, children, ...resets } = _route;
const wholePath = getWholePath(path, basename, parent?.path);
const newRoute = {
...resets,
path: getValidPathname(wholePath),
parent,
_level,
_relativePath: path
};
if (items) {
newRoute.items = _cloneRoutes(items, newRoute, _level + 1);
}
if (children) {
newRoute.children = _cloneRoutes(children, newRoute, _level + 1);
}
return newRoute;
});
}
return _cloneRoutes(routes, parent, _level);
}
function rankRouteBranches(branches) {
branches.sort((a, b) => b.score - a.score);
}
/**
*cCalculate some state data
* @param inputRoutes
* @param permissionList
* @returns
*/
function computedNewState(config) {
const {
inputRoutes,
permissionList,
permissionMode,
hasAuth,
beforeEachMount,
basename,
location
} = config;
const authInputRoutes = computeRoutesConfig({
routes: inputRoutes,
permissionList,
permissionMode,
hasAuth,
beforeEachMount
});
const flattenBranches = flattenRoutesFn(authInputRoutes, undefined, true);
/**
* Calculate routes permission when permissionMode is 'children'
*/
if (permissionMode === "children" && hasAuth) {
flattenBranches.forEach(_branch => {
const route = _branch.route;
const currentIsHasAuth = route._currentIsHasAuth;
if (!currentIsHasAuth) {
return;
}
/**
* If the child route has permission, the parent route also has permission
*/
let _currentRoute = route;
while (_currentRoute) {
_currentRoute._isHasAuth = true;
_currentRoute._component = _currentRoute._currentComponent;
_currentRoute = _currentRoute.parent;
}
});
}
// mixin into the notFound page
mixinNotFoundPage(flattenBranches, basename, authInputRoutes);
rankRouteBranches(flattenBranches);
const flattenRoutes = flattenBranches.map(i => i.route);
const routesMap = routesMapFn(flattenBranches);
const currentRoute = getCurrentRoute(
location.pathname,
routesMap,
flattenRoutes
);
const currentPathRoutes = getCurrentPathRoutes(currentRoute);
return {
authInputRoutes,
flattenRoutes: flattenRoutes,
routesMap,
currentRoute,
currentPathRoutes,
beforeEachMount
};
}
/**
* flattenRoutes score use react-router methods
*/
const paramRe = /^:\w+$/;
const dynamicSegmentValue = 3;
const indexRouteValue = 2;
const emptySegmentValue = 1;
const staticSegmentValue = 10;
const splatPenalty = -2;
const isSplat = s => s === "*";
function computeScore(path, index) {
let segments = path.split("/");
let initialScore = segments.length;
if (segments.some(isSplat)) {
initialScore += splatPenalty;
}
if (index) {
initialScore += indexRouteValue;
}
return segments
.filter(s => !isSplat(s))
.reduce(
(score, segment) =>
score +
(paramRe.test(segment)
? dynamicSegmentValue
: segment === ""
? emptySegmentValue
: staticSegmentValue),
initialScore
);
}
/**
* flatten the react router array recursively
* whether all is true, includes sub routes
*/
const flattenRoutesFn = (arr, parent, all) => {
return arr.reduce((prev, nextRoute) => {
if (parent) {
nextRoute.parent = parent;
}
const routeBranch = {
path: nextRoute.path,
route: nextRoute,
score: computeScore(nextRoute.path, false)
};
if (Array.isArray(nextRoute.items) || Array.isArray(nextRoute.children)) {
let _routes = prev.concat(routeBranch);
if (Array.isArray(nextRoute.items)) {
_routes = _routes.concat(
flattenRoutesFn(nextRoute.items, nextRoute, all)
);
}
if (Array.isArray(nextRoute.children) && all) {
_routes = _routes.concat(
flattenRoutesFn(nextRoute.children, nextRoute, all)
);
}
return _routes;
} else {
return prev.concat(routeBranch);
}
}, []);
};
// name => mapping of route
const routesMapFn = flattenRoutes => {
const routesInterMap = flattenRoutes.reduce(
(_routesInterMap, routeBranch) => {
const { route: nextRoute } = routeBranch;
const { name, path } = nextRoute;
if (_routesInterMap[name]) {
throw new Error(
`Router config 'name' isn't unique, route name: "${name}", route path: "${path}"`
);
}
// the route has params
const existPathRoute = _routesInterMap[path];
if (existPathRoute) {
// 判断上一个是不是真正的路由父级
const parentAbs = existPathRoute[existPathRoute.length - 1];
if (parentAbs?.name && parentAbs.name === nextRoute.parentAbs?.name) {
_routesInterMap[path] = [...existPathRoute, nextRoute];
} else {
throw new Error(
`There are routes at the same level with the same path, route name: "${name}", route path: "${path}"`
);
}
} else {
_routesInterMap[path] = [nextRoute];
}
_routesInterMap[name] = nextRoute;
return _routesInterMap;
},
{}
);
const routesMap = {};
proxyRoutesMapFromTarget(routesMap, routesInterMap);
return routesMap;
};
// convert '/a/b/c/' to '/a/b/c'
function getValidPathname(pathname) {
if (!pathname) {
return pathname;
}
if (pathname.endsWith("/")) {
return pathname.slice(0, -1);
}
return pathname;
}
/** find the current route object through the path */
function getCurrentRoute(
pathname = window.location.pathname,
routesMap,
flattenRoutes
) {
// console.log(routesMap);
// first look from the outermost routesMap
pathname = getValidPathname(pathname);
let currentRoute = routesMap[pathname];
// TODO 找通配符的 后续优化
if (!currentRoute) {
// 有通配符的路径
const route = flattenRoutes.find(_route => {
let match = matchPath(
{
path: _route.path
},
pathname
);
if (match) {
return true;
}
return false;
});
if (route) {
return route;
}
}
if (!currentRoute) {
// 默认404
currentRoute = {
_isHasAuth: true,
_relativePath: "",
_level: 0,
name: "404",
path: pathname,
title: "404",
meta: {},
component: () => {
return /*#__PURE__*/ React.createElement(NoAuth, {
code: "404"
});
},
_route: {
_level: 0,
_relativePath: "",
name: "404",
path: pathname,
title: "404",
component: () => {
return /*#__PURE__*/ React.createElement(NoAuth, {
code: "404"
});
}
}
};
}
return currentRoute;
}
/**
* Get the route path (a collection of parent routes and child routes)
* @param currentRoute
* @returns
*/
function getCurrentPathRoutes(currentRoute) {
const routes = [];
let pathRoute = currentRoute;
while (pathRoute) {
routes.unshift(pathRoute);
pathRoute = pathRoute.parent;
}
return routes;
}
function executeEventCbs(option) {
const { to, from, callbacks, finish } = option;
function executeNextCb(cbIndex = 0) {
const nextCb = callbacks[cbIndex];
if (!nextCb) {
return finish();
} else {
nextCb.fn(to, from, () => {
executeNextCb(cbIndex + 1);
});
}
}
if (callbacks.length === 0) {
finish();
} else {
executeNextCb(0);
}
}
function getIsHasAuthByStrCode(code, permissionList) {
return permissionList.includes(code);
}
function getIsHasAuthByFnCode(code, route) {
return code(route);
}
/**
* if `children` is mode,
* isHasAuth are calculated after generating `flattroutes`
*/
function getIsHasAuth({ code, permissionList, hasAuth, route }) {
if (!hasAuth) {
// 未配置权限
return true;
}
if (!code) {
// 未配置code默认有权限
return true;
}
if (Array.isArray(code)) {
if (code.length === 0) {
return true;
}
return code.some(_code => {
return getIsHasAuthByStrCode(_code, permissionList);
});
}
if (code instanceof Function) {
return !!getIsHasAuthByFnCode(code, route);
}
return getIsHasAuthByStrCode(code, permissionList);
}
function computeRoutesConfig(config) {
const {
routes,
permissionList = [],
permissionMode,
hasAuth,
beforeEachMount,
parent,
parentAbs
} = config;
return routes.map(route => {
const {
component: Component,
code = "",
children,
items,
beforeEnter,
redirect,
meta,
type
} = route;
let _children = [];
let _items = [];
let CurrentComponent = Component;
if (!CurrentComponent) {
CurrentComponent = RedirectChild;
}
const props = {};
if (beforeEnter || beforeEachMount) {
props.beforeEnter = beforeEnter;
props.beforeEachMount = beforeEachMount;
props.key = route.name; // users switch between routes to avoid incorrect rendering due to the same key after route switching
props._route = route;
props.Component = CurrentComponent;
CurrentComponent = GeneratorHookCom;
}
const currentIsHasAuth = getIsHasAuth({
code,
permissionList,
hasAuth,
route
});
// 默认父级无权限,则择机也无权限
const isHasAuth = parent?._isHasAuth === false ? false : currentIsHasAuth;
const newRoute = {
...route,
parent,
parentAbs,
props,
meta: meta || {},
items: [],
children: [],
_route: route,
_isHasAuth: isHasAuth,
_currentComponent: CurrentComponent,
_currentIsHasAuth: currentIsHasAuth
};
// Process of sub routes
if (children) {
_children = computeRoutesConfig({
routes: children,
permissionList,
permissionMode,
hasAuth,
beforeEachMount,
parent: newRoute,
parentAbs: newRoute
});
}
// Process peer routes
if (items) {
_items = computeRoutesConfig({
routes: items,
permissionList,
permissionMode,
hasAuth,
beforeEachMount,
parent: newRoute,
parentAbs: parentAbs
});
}
const _itemsAndChildren = [..._items, ..._children];
if (!isHasAuth) {
const returnRoute = {
...newRoute,
children: _children,
items: _items,
_component: NoAuth,
_itemsAndChildren
};
return returnRoute;
}
if (redirect) {
return {
...newRoute,
children: _children,
items: _items,
_component: () =>
/*#__PURE__*/ React.createElement(Navigate$1, {
to: redirect,
replace: true
}),
_itemsAndChildren
};
}
if (type === "null") {
return {
...newRoute,
children: _children,
items: _items,
_component: undefined,
_itemsAndChildren
};
}
if (CurrentComponent) {
return {
...newRoute,
children: _children,
items: _items,
_component: CurrentComponent,
_itemsAndChildren
};
} else {
const redirectPath = handleRedirectPath(route, permissionList, hasAuth);
if (redirectPath) {
let replace = false;
// 父级也没配置component,则会进行多次重定向进行replace, 以便浏览器回退行为
if (!route.parent?.component) {
replace = true;
}
return {
...newRoute,
children: _children,
items: _items,
_component: () =>
/*#__PURE__*/ React.createElement(Navigate$1, {
to: redirectPath,
replace: replace
}),
_itemsAndChildren
};
}
return {
...newRoute,
items: [],
children: [],
_component: NoAuth
};
}
});
}
function getCurrentRouteCbsByEvent(routeEvent, pathname, routeHooks) {
return routeHooks.filter(i => {
return i.name === routeEvent && i.pathname === pathname;
});
}
function pathStartMarkTransform(path) {
if (path === "/") {
return path;
}
return path.replace(/\/*\**$/gm, "");
}
/**
* when jump route,Remove the '*'
* @param to
* @returns
*/
function getRealTo(to) {
if (typeof to === "string") {
return pathStartMarkTransform(to);
}
const { pathname } = to;
if (pathname) {
return {
...to,
pathname: pathStartMarkTransform(pathname)
};
}
return to;
}
const handleRedirectPath = (route, permissionList, hasAuth, permissionMode) => {
// Return to the first menu(route) item with permission
const { items } = route;
if (!items) {
return "";
}
// No configuration permission, return to the first one child route
if (!hasAuth) {
return items[0].path;
}
// 找带有index的路由
const indexRoute = items.find(i => i.index);
if (indexRoute) {
return indexRoute.path;
}
let redirectPath = "";
// find the first one route with permission
for (let i = 0; i < items?.length; i++) {
const childRoute = items[i];
const { code, path } = childRoute;
if (!code) {
// if code is false, the default is permission
redirectPath = path;
break;
}
if (
getIsHasAuth({
code,
permissionList,
hasAuth,
route: childRoute
})
) {
redirectPath = path;
break;
}
}
return redirectPath;
};
function mixinNotFoundPage(flattenBranches, basename, authInputRoutes) {
const notFoundPath = getWholePath("*", basename);
const hasNotFoundPage = flattenBranches.some(({ path }) => {
if (path === notFoundPath) {
return true;
}
return false;
});
if (hasNotFoundPage) {
return;
}
const notFoundPage = {
name: "notFound",
title: "notFound",
meta: {},
path: notFoundPath,
component: NotFound,
_isHasAuth: true,
_component: NotFound,
_relativePath: notFoundPath,
_level: 0,
_route: {
_relativePath: notFoundPath,
_level: 0,
name: "notFound",
title: "notFound",
meta: {},
path: notFoundPath,
component: NotFound
}
};
authInputRoutes.push(notFoundPage);
flattenBranches.push({
path: notFoundPath,
score: flattenBranches.length,
route: notFoundPage
});
}
// determine whether it is a react component
// react Components are characterized by functions
function isComponent(component) {
if (component instanceof Function) {
return true;
}
return false;
}
function isString(str) {
if (typeof str === "string") {
return true;
}
return false;
}
/**
* 1、To support ts, users are prompted when writing code
* 2、Modify global configuration
* 3、Add '_isDefined' attribute, Used by MRouter to defined whether the object has been called defineRouterConfig
* @param routerConfig
* @returnsRouterBaseConfigI
*/
let _defineId = 0;
function defineRouterConfig(routerConfig) {
const navigateRef = {
current: null
};
const { LoadingComponent, ..._config } = routerConfig;
_defineId = _defineId + 1;
if (LoadingComponent) {
setChangeable({
LoadingComponent
});
}
/** add '_isDefined' attribute */
const config = {
..._config,
_isDefined: true,
_defineId: _defineId,
// Placeholder
navigate: () => {
throw new Error("YSRouter navigate is not initialized");
},
_navigateRef: navigateRef
};
Object.defineProperty(config, "navigate", {
get() {
return (
navigateRef.current ||
(() => {
throw new Error("YSRouter navigate is not initialized");
})
);
}
});
return config;
}
function flattenArr(ary) {
if (!ary) {
return [];
}
return ary.reduce((pre, cur) => {
return pre.concat(Array.isArray(cur) ? flattenArr(cur) : cur);
}, []);
}
function newInputRoutesState(inputRoutes) {
const routesMap = {};
function _cloneInputRoutes(inputRoutes, parent) {
if (!inputRoutes) {
return [];
}
return inputRoutes.map(i => {
const _route = {
...i,
items: [],
children: [],
parent: parent
};
_route.items = _cloneInputRoutes(i.items, _route);
_route.children = _cloneInputRoutes(i.children, _route);
routesMap[i.name] = _route;
return _route;
}, []);
}
return {
inputRoutes: _cloneInputRoutes(inputRoutes),
routesMap: routesMap
};
}
/**
* add routes operation
* @param state MRouterStateI
* @param payload RouteTypeI[]
* @returns MRouterStateI
*/
function addRoutesAction(state, payload) {
let hasChange = false;
const newRoutes = payload;
const { routesMap, inputRoutes } = newInputRoutesState(state.inputRoutes);
newRoutes.forEach(_route => {
const { path, name, parentName } = _route;
if (routesMap[path] || routesMap[name]) {
throw new Error(`新增路由 ${name} ${path} 已经存在,请修改`);
}
if (parentName) {
const _parentRoute = routesMap[parentName];
if (_parentRoute) {
const route = cloneRoutes({
routes: [_route],
parent: _parentRoute,
_level: _parentRoute._level + 1
});
_parentRoute.items = _parentRoute.items || [];
_parentRoute.items.push(route[0]);
hasChange = true;
}
} else {
// 根路径插入
const route = cloneRoutes({
routes: [_route],
_level: 0
});
inputRoutes.push(route[0]);
hasChange = true;
}
});
if (hasChange) {
const newState = state._getNewStateByNewInputRoutes(inputRoutes);
return {
...state,
...newState,
inputRoutes: [...inputRoutes]
};
}
return state;
}
/**
* update routes operation
* @param state MRouterStateI
* @param payload RouteTypeI[]
* @returns MRouterStateI
*/
function updateRoutesAction(state, payload) {
let hasChange = false;
// const { basename } = state;
const newRoutesPayload = payload;
const { routesMap, inputRoutes } = newInputRoutesState(state.inputRoutes);
newRoutesPayload.forEach(({ routeName, routeData }) => {
const route = routesMap[routeName];
if (route) {
// 如果parent存在,则不是根节点
const parent = route.parent;
const _newRouteData = {
...route,
...routeData
};
const newRouteData = cloneRoutes({
routes: [_newRouteData],
parent,
_level: (parent?._level || 0) + 1,
basename: state.basename
});
if (!parent) {
Object.assign(route, newRouteData[0]);
hasChange = true;
}
if (parent && parent.items) {
parent.items.splice(parent.items.indexOf(route), 1, newRouteData[0]);
hasChange = true;
} else if (parent && parent.children) {
parent.children.splice(
parent.children.indexOf(route),
1,
newRouteData[0]
);
hasChange = true;
}
}
});
if (hasChange) {
const newState = state._getNewStateByNewInputRoutes(inputRoutes);
return {
...state,
...newState,
inputRoutes: [...inputRoutes]
};
}
return state;
}
/**
* remove routes operation
* @param state MRouterStateI
* @param payload RouteTypeI[]
* @returns MRouterStateI
*/
function removeRoutesAction(state, payload) {
let hasChange = false;
const routeNames = payload;
const { routesMap, inputRoutes } = newInputRoutesState(state.inputRoutes);
routeNames.forEach(routeName => {
const _route = routesMap[routeName];
if (_route) {
// 如果parent存在,则不是根节点
const parent = _route.parent;
if (!parent) {
const index = inputRoutes.indexOf(_route);
if (index > -1) {
inputRoutes.splice(index, 1);
hasChange = true;
}
}
if (parent && parent.items) {
const index = parent.items.indexOf(_route);
if (index > -1) {
parent.items.splice(index, 1);
hasChange = true;
}
}
}
});
if (hasChange) {
const newState = state._getNewStateByNewInputRoutes(inputRoutes);
return {
...state,
...newState,
inputRoutes: [...inputRoutes]
};
} else {
return state;
}
}
var RouteNavTypeEnum;
(function(RouteNavTypeEnum) {
RouteNavTypeEnum[(RouteNavTypeEnum["menu"] = 0)] = "menu";
RouteNavTypeEnum[(RouteNavTypeEnum["step"] = 1)] = "step";
})(RouteNavTypeEnum || (RouteNavTypeEnum = {}));
var RouterActionEnum;
(function(RouterActionEnum) {
RouterActionEnum["UPDATE_INPUT_ROUTES"] = "UPDATE_INPUT_ROUTES";
RouterActionEnum["UPDATE_CURRENT_ROUTE"] = "UPDATE_CURRENT_ROUTE";
RouterActionEnum["UPDATE_STATE"] = "UPDATE_STATE";
RouterActionEnum["ADD_ROUTES"] = "ADD_ROUTES";
RouterActionEnum["REMOVE_ROUTES"] = "REMOVE_ROUTES";
RouterActionEnum["UPDATE_ROUTES"] = "UPDATE_ROUTES";
})(RouterActionEnum || (RouterActionEnum = {}));
const MRouterContext = /*#__PURE__*/ React.createContext({
state: {
inputRoutes: [],
authInputRoutes: [],
permissionList: [],
routesMap: {},
flattenRoutes: []
},
methods: {}
});
MRouterContext.displayName = "MRouterContext";
function MRouterReducer(state, action) {
const { type, payload } = action;
switch (type) {
case RouterActionEnum.ADD_ROUTES: {
return addRoutesAction(state, payload);
}
case RouterActionEnum.REMOVE_ROUTES: {
return removeRoutesAction(state, payload);
}
case RouterActionEnum.UPDATE_ROUTES: {
return updateRoutesAction(state, payload);
}
case RouterActionEnum.UPDATE_INPUT_ROUTES: {
return {
...state,
inputRoutes: payload
};
}
case RouterActionEnum.UPDATE_CURRENT_ROUTE: {
return {
...state,
currentRoute: payload
};
}
case RouterActionEnum.UPDATE_STATE: {
return {
...state,
...payload
};
}
default: {
return {
...state
};
}
}
}
function useRouterState() {
return React.useContext(MRouterContext).state;
}
/** Dynamically add routing method */
function useAddRoutes() {
return React.useContext(MRouterContext).methods.addRoutes;
}
function useRemoveRoutes() {
return React.useContext(MRouterContext).methods.removeRoutes;
}
function useUpdateRoutes() {
return React.useContext(MRouterContext).methods.updateRoutes;
}
// Initialized data to prevent double calculation
// defineRouterConfig may be called multiple times in the same application
let initialStateMap = new Map();
function getSameQueryData(prevData, currentData) {
return (
prevData.basename === currentData.basename &&
prevData.hasAuth === currentData.hasAuth &&
prevData.beforeEachMount === currentData.beforeEachMount &&
prevData.inputRoutes === currentData.inputRoutes &&
prevData.permissionList === currentData.permissionList
);
}
function getInitialState(currentQueryData) {
const {
inputRoutes,
hasAuth,
permissionList,
permissionMode,
beforeEachMount,
basename,
location,
_defineId
} = currentQueryData;
const prevData = initialStateMap.get(_defineId);
if (prevData) {
const isSameQueryData = getSameQueryData(
prevData.queryData,
currentQueryData
);
if (isSameQueryData) {
return prevData.initialData;
}
}
const _initialState = computedNewState({
inputRoutes,
permissionList,
permissionMode,
hasAuth,
beforeEachMount,
basename,
location
});
initialStateMap.set(_defineId, {
queryData: currentQueryData,
initialData: _initialState
});
return _initialState;
}
function replaceHistoryMethods(history, allExecuteEventCbs, oldHistoryMethods) {
history.go = delta => {
allExecuteEventCbs(() => {
const res = oldHistoryMethods.go(delta);
// history.go = oldHistoryMethods.go;
return res;
});
};
history.push = (to, state) => {
to = getRealTo(to);
allExecuteEventCbs(() => {
const res = oldHistoryMethods.push(to, state);
// history.push = oldHistoryMethods.push;
return res;
}, to);
};
history.replace = (to, state) => {
to = getRealTo(to);
allExecuteEventCbs(() => {
const res = oldHistoryMethods.replace(to, state);
// history.replace = oldHistoryMethods.replace;
return res;
}, to);
};
history.back = () => {
allExecuteEventCbs(() => {
const res = oldHistoryMethods.go(-1);
// history.back = oldHistoryMethods.back;
return res;
});
};
history.forward = () => {
allExecuteEventCbs(() => {
const res = oldHistoryMethods.go(1);
// history.forward = oldHistoryMethods.forward;
return res;
});
};
}
function computedUseRoutesConfig(routes) {
const _routes = routes.map(route => {
let _routeConfig;
let _itemsRouteConfig = [];
const {
_component: Component,
path,
items,
children,
_isHasAuth,
props
} = route;
if (Component) {
const LoadingCmp = changeable.LoadingComponent;
if (!_isHasAuth) {
/** Without permission, the child also has no permission */
return {
path: path.endsWith("*") ? path : `${path}/*`,
element: /*#__PURE__*/ React.createElement(
Suspense,
{
fallback: /*#__PURE__*/ React.createElement(LoadingCmp, null)
},
/*#__PURE__*/ React.createElement(Component, {
...props
})
)
};
}
_routeConfig = {
path,
element: /*#__PURE__*/ React.createElement(
Suspense,
{
fallback: /*#__PURE__*/ React.createElement(LoadingCmp, null)
},
/*#__PURE__*/ React.createElement(Component, {
...props
})
)
};
if (children) {
_routeConfig.children = computedUseRoutesConfig(children);
}
if (items) {
_itemsRouteConfig = computedUseRoutesConfig(items);
}
}
const nextRoutes = [_routeConfig, ..._itemsRouteConfig].filter(i => {
return i !== undefined;
});
return nextRoutes;
});
return flattenArr(_routes);
}
const DEFAULT_PERMISSION_LIST = [];
const InternalMRouterContextProvider = (
{
permissionList = DEFAULT_PERMISSION_LIST,
permissionMode = "parent",
routerConfig,
hasAuth,
children
},
ref
) => {
const history = useHistory();
const location = useLocation$1();
const locationRef = useRef(location);
const oldHistoryMethods = useHistoryMethods();
const routeHooksRef = useRouteHooksRef();
const navigate = useNavigate();
const {
routes = [],
basename = "/",
beforeEachMount,
autoDocumentTitle = false,
_navigateRef
} = routerConfig;
// expose navigate to external use
if (_navigateRef) {
_navigateRef.current = navigate;
}
const inputRoutes = useMemo(() => {
return cloneRoutes({
routes,
basename
});
}, [basename, routes]);
const inputRoutesRef = useRef(inputRoutes);
const initialStateRef = useRef(null);
const initialState = useMemo(() => {
if (!initialStateRef.current) {
initialStateRef.current = getInitialState({
inputRoutes,
permissionList,
permissionMode,
hasAuth,
beforeEachMount,
basename,
location: locationRef.current,
_defineId: routerConfig._defineId
});
}
return initialStateRef.current;
}, [
basename,
beforeEachMount,
hasAuth,
inputRoutes,
permissionList,
permissionMode,
routerConfig._defineId
]);
const getNewStateByNewInputRoutesRef = useRef(null);
const _getNewStateByNewInputRoutes = useCallback(
_inputRoutes => {
return computedNewState({
inputRoutes: _inputRoutes,
permissionList,
permissionMode,
hasAuth,
beforeEachMount,
basename,
location: location
});
},
[
basename,
beforeEachMount,
hasAuth,
location,
permissionList,
permissionMode
]
);
getNewStateByNewInputRoutesRef.current = _getNewStateByNewInputRoutes;
// initialization
const [state, dispatch] = useReducer(MRouterReducer, {
...initialState,
inputRoutes,
permissionList,
permissionMode,
hasAuth,
basename,
_getNewStateByNewInputRoutes: getNewStateByNewInputRoutesRef.current
});
/**
* listen router change,set currentRoute
* Put the updated currentRoute in history listen updates in batches to reduce the number of updates
*/
useImperativeHandle(ref, () => {
return {
updateCurrentRoute(location) {
const { pathname } = location;
const prevRoute = state.currentRoute;
const currentRoute = getCurrentRoute(
pathname,
state.routesMap,
state.flattenRoutes
);
let currentPathRoutes = state.currentPathRoutes;
if (currentRoute !== state.currentRoute) {
currentPathRoutes = getCurrentPathRoutes(currentRoute);
}
dispatch({
type: RouterActionEnum.UPDATE_STATE,
payload: {
currentRoute,
currentPathRoutes,
prevRoute
}
});
}
};
});
useLayoutEffect(() => {
// filter routes without permission
// used to judge initialization or update. If they are equal, only currentRoute needs to be calculated
if (
state.permissionList === permissionList &&
state.permissionMode === permissionMode &&
hasAuth === state.hasAuth &&
state.inputRoutes === inputRoutes &&
state.beforeEachMount === beforeEachMount
) {
return;
}
// if inputRoutes change, the incoming inputRoutes shall prevail
let _inputRoutes = state.inputRoutes;
if (
inputRoutesRef.current === inputRoutes &&
state.permissionList === permissionList &&
state.permissionMode === permissionMode &&
hasAuth === state.hasAuth &&
state.beforeEachMount === beforeEachMount
) {
// Equal, indicating that state.inputRoutes has changed, is add remove and update routes
return;
} else {
// if not Equal, record the value of inputRoutes for next comparison
inputRoutesRef.current = inputRoutes;
_inputRoutes = inputRoutes;
}
const {
authInputRoutes,
flattenRoutes,
routesMap,
currentRoute,
currentPathRoutes
} = computedNewState({
inputRoutes: _inputRoutes,
permissionList,
permissionMode,
hasAuth,
beforeEachMount,
basename,
location
});
dispatch({
type: RouterActionEnum.UPDATE_STATE,
payload: {
permissionList,
permissionMode,
authInputRoutes,
routesMap,
flattenRoutes,
currentRoute,
currentPathRoutes,
basename,
beforeEachMount,
inputRoutes: _inputRoutes
}
});
}, [
state.inputRoutes,
inputRoutes,
permissionList,
state.permissionList,
permissionMode,
state.permissionMode,
hasAuth,
state.hasAuth,
basename,
beforeEachMount,
location,
state.beforeEachMount
]);
// auto setting document.title
useEffect(() => {
if (!autoDocumentTitle) {
return;
}
let title = "";
if (typeof autoDocumentTitle === "boolean") {
title = state.currentPathRoutes
.map(i => {
return i.title;
})
.join("-");
} else if (typeof autoDocumentTitle === "function") {
title = autoDocumentTitle(state.currentPathRoutes);
}
document.title = title;
}, [autoDocumentTitle, state.currentPathRoutes]);
const allExecuteEventCbs = useCallback(
(historyCb, to) => {
if (typeof to !== "string") {
to = to?.pathname;
}
const pathname = window.location.pathname;
const beforeRouterLeaveCbs = getCurrentRouteCbsByEvent(
"BeforeRouterLeave",
pathname,
routeHooksRef.current
);
if (state.currentRoute?.beforeLeave) {
beforeRouterLeaveCbs.unshift({
name: "BeforeRouterLeave",
pathname: state.currentRoute.path,
fn: state.currentRoute.beforeLeave
});
}
if (beforeRouterLeaveCbs.length) {
executeEventCbs({
to: getCurrentRoute(to, state.routesMap, state.flattenRoutes),
from: getCurrentRoute(pathname, state.routesMap, state.flattenRoutes),
callbacks: beforeRouterLeaveCbs,
finish: () => {
return historyCb();
}
});
} else {
return historyCb();
}
},
[
routeHooksRef,
state.currentRoute.beforeLeave,
state.currentRoute.path,
state.routesMap,
state.flattenRoutes
]
);
useLayoutEffect(() => {
// Intercept the methods used in history in useNavigator
replaceHistoryMethods(history, allExecuteEventCbs, oldHistoryMethods);
}, [allExecuteEventCbs, history, oldHistoryMethods]);
const addRoutes = useCallback(newRoutes => {
dispatch({
type: RouterActionEnum.ADD_ROUTES,
payload: newRoutes
});
}, []);
const removeRoutes = useCallback(routeNames => {
dispatch({
type: RouterActionEnum.REMOVE_ROUTES,
payload: routeNames
});
}, []);
const updateRoutes = useCallback(routes => {
dispatch({
type: RouterActionEnum.UPDATE_ROUTES,
payload: routes
});
}, []);
const updateCurrentRoute = useCallback(currentRoute => {
if (!currentRoute) {
return;
}
dispatch({
type: RouterActionEnum.UPDATE_CURRENT_ROUTE,
payload: currentRoute
});
}, []);
const routesConfig = useMemo(() => {
const _routesConfig = computedUseRoutesConfig(state.authInputRoutes);
return _routesConfig;
}, [state.authInputRoutes]);
// console.log(routesConfig);
const routesChildren = useRoutes(routesConfig);
const renders = useMemo(() => {
return children ? children(routesChildren) : routesChildren;
}, [children, routesChildren]);
return /*#__PURE__*/ React.createElement(
MRouterContext.Provider,
{
value: {
state,
methods: {
addRoutes,
updateCurrentRoute,
removeRoutes,
updateRoutes
}
}
},
renders
);
};
const MRouterContextProvider = /*#__PURE__*/ React.forwardRef(
InternalMRouterContextProvider
);
MRouterContextProvider.displayName = "MRouterContextProvider";
const CoreRouter = ({
permissionList,
permissionMode,
wrapComponent: WrapComponent,
hasAu