UNPKG

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
/** * 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