UNPKG

@ovine/core

Version:

Build flexible admin system with json.

238 lines (237 loc) 11.4 kB
/** * APP 路由相关组件 * 优化: 由于route读取数据时,会有短暂的 404 * BUG: 页面切换太快时,会导致页面报错 * Uncaught (in promise) Error: [mobx-state-tree] Cannot modify 'ErrorDetail[]@/errorData [dead]', the object is protected and can only be modified by using an action. */ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __rest = (this && this.__rest) || function (s, e) { var t = {}; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) t[p] = s[p]; if (s != null && typeof Object.getOwnPropertySymbols === "function") for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) t[p[i]] = s[p[i]]; } return t; }; import { Spinner } from 'amis'; import { eachTree, flattenTree } from 'amis/lib/utils/helper'; import { isFunction, map, get, cloneDeep, omit, pick } from 'lodash'; import React, { createContext, lazy, useContext, useMemo, Suspense, useState, useEffect, isValidElement, useRef, } from 'react'; import { Redirect, Route, Switch } from 'react-router-dom'; import { app } from "../app"; import NotFound from "../components/404"; import { Amis } from "../components/amis/schema"; import { useDebounceRender } from "../components/debounce_render"; import ErrorBoundary from "../components/error_boundary"; import { isSubStr } from "../utils/tool"; import { setRoutesConfig } from "./config"; import { getNodePath, getPageMockSource, getPagePreset, getRoutePath, currPath, getPageFileAsync, } from "./exports"; import { clearRouteStore, getAsideMenus, getAuthRoutes } from "./limit"; import { checkLimitByKeys } from "./limit/exports"; const PageSpinner = React.createElement(Spinner, { overlay: true, show: true, size: "lg", key: "pageLoading" }); // 根据 path,pathToComponent 参数 懒加载 `pages/xxx` 组件 export const getPageAsync = (option) => { return lazy(() => getPageFileAsync(option).then((file) => { const { default: content = {}, schema, getSchema } = file; const defaultSchema = { type: 'wrapper', body: '请传入正确的 schema', }; const compProps = {}; if (isFunction(content)) { compProps.LazyFileComponent = content; } else { if (schema || getSchema) { content.schema = isFunction(getSchema) ? getSchema(option) : schema || defaultSchema; } if (!content.schema) { content.schema = defaultSchema; } compProps.lazyFileAmisProps = content; } return { default: () => React.createElement(PrestComponent, Object.assign({}, option, compProps)), }; })); }; // 登录路由拦截 export const PrivateRoute = (props) => { const { onAuth, onRedirect, redirect, children } = props, rest = __rest(props, ["onAuth", "onRedirect", "redirect", "children"]); const [isAuth, setAuth] = useState(null); const isMounted = useRef(null); const redirectPath = (onRedirect ? onRedirect() : redirect) || app.constants.loginRoute; const checkAuth = () => __awaiter(void 0, void 0, void 0, function* () { if (isFunction(onAuth)) { const authRes = yield onAuth(); if (isMounted.current) { setAuth(authRes); } return; } setAuth(true); }); useEffect(() => { isMounted.current = true; checkAuth(); return () => { isMounted.current = false; }; }, []); if (isAuth === null) { return React.createElement("div", null); } return (React.createElement(Route, Object.assign({}, rest, { render: ({ location }) => { if (isAuth) { return children; } if (redirectPath) { return (React.createElement(Redirect, { to: { pathname: redirectPath, state: { from: location }, } })); } return React.createElement("div", null, "unauthorized route."); } }))); }; // usePresetContext 可获取 preset 值,与 checkLimit 校验权限 方法 const PresetContext = createContext({ route: {}, }); export const usePresetContext = () => { const preset = useContext(PresetContext); const checkLimit = (keys, option) => checkLimitByKeys(keys, Object.assign({ nodePath: preset.nodePath }, option)); return Object.assign(Object.assign({}, preset), { checkLimit }); }; // 将 preset 注入组件,可全局通过 usePresetContext 获取 preset 值 const PrestComponent = (props) => { const { LazyFileComponent, lazyFileAmisProps, RouteComponent } = props, rest = __rest(props, ["LazyFileComponent", "lazyFileAmisProps", "RouteComponent"]); const { path, exact, children, pathToComponent, nodePath: propNodePath } = rest; const preset = useMemo(() => { const fileOption = { path, pathToComponent, nodePath: propNodePath }; const mockSource = !app.env.isMock ? undefined : getPageMockSource(fileOption); const nodePath = getNodePath(fileOption); const presetConf = cloneDeep(Object.assign(Object.assign({}, pick(rest, ['limits', 'apis'])), (getPagePreset(fileOption) || get(lazyFileAmisProps, 'schema.preset') || {}))); presetConf.nodePath = exact && children && path ? path : nodePath; map(presetConf.apis, (api) => { // 自动注入规则 if (api.url && api.mock !== false && !api.mockSource && mockSource) { api.mockSource = mockSource[api.api || api.url] || mockSource; } }); return presetConf; }, [path, pathToComponent, propNodePath]); const contextValue = Object.assign(Object.assign({}, preset), { route: omit(rest, Object.keys(preset)) }); let Component = React.createElement("div", null, "Not Found \u8BF7\u68C0\u67E5\u8DEF\u7531\u8BBE\u7F6E"); if (LazyFileComponent) { Component = React.createElement(LazyFileComponent, Object.assign({}, rest)); } if (RouteComponent) { Component = React.createElement(RouteComponent, Object.assign({}, rest)); } if (lazyFileAmisProps) { const contextRef = get(lazyFileAmisProps, 'props.contextRef'); if (isFunction(contextRef)) { contextRef(contextValue); } lazyFileAmisProps.schema.preset = Object.assign(Object.assign({}, lazyFileAmisProps.schema.preset), preset); Component = React.createElement(Amis, Object.assign({}, rest, lazyFileAmisProps)); } return React.createElement(PresetContext.Provider, { value: contextValue }, Component); }; // 处理每个路由,包裹 PrestComponent 组件 export const PrestRoute = (props) => { var _a; const { component, children, withSuspense = true, fallback = PageSpinner, path = '', exact = true, debounceRoute = 0 } = props, rest = __rest(props, ["component", "children", "withSuspense", "fallback", "path", "exact", "debounceRoute"]); const routePath = getRoutePath(path); const locationKey = ((_a = rest === null || rest === void 0 ? void 0 : rest.location) === null || _a === void 0 ? void 0 : _a.key) || ''; const keyRef = useRef(''); if (locationKey) { keyRef.current = locationKey; } const RouteComponent = (React.createElement(Route, Object.assign({}, rest, { path: routePath, exact: exact, component: !component ? getPageAsync(props) : () => React.createElement(PrestComponent, Object.assign({}, props, { RouteComponent: component })) }), isValidElement(children) ? children : null)); const getRouteComponent = () => { if (exact && !isSubStr(routePath, ':') && routePath !== window.location.pathname) { return React.createElement(Redirect, { to: app.constants.notFound.route }); } if (withSuspense) { return (React.createElement(ErrorBoundary, { type: "page" }, React.createElement(Suspense, { fallback: fallback }, RouteComponent))); } return RouteComponent; }; // TODO: 在 qiankun 中, 每次点击路由切换会,刷新强制触发页面,两次更新。在飞 qiankun 页面下,没问题。 // 该解决方案会导致页面切换时会闪一下 const CachedRoute = useDebounceRender({ getComponent: getRouteComponent, wait: debounceRoute, }, [keyRef.current]); return debounceRoute ? CachedRoute : getRouteComponent(); }; // TODO: 支持自定义 404 const NotFoundRoute = () => { let Component = NotFound; const notFoundFilePath = app.constants.notFound.pagePath; if (notFoundFilePath) { try { Component = lazy(() => getPageFileAsync({ nodePath: currPath(notFoundFilePath, '404'), })); } catch (_) { Component = NotFound; } } return React.createElement(Route, { path: "*", component: Component }); }; // 将 routeConfig 转换为 route export const AppMenuRoutes = (props) => { const menuRoutes = []; const { debounceRoute, pathPrefix = '', authRoutes, fallback: FallBack } = props; // eslint-disable-next-line eachTree(authRoutes, (item) => { const { path, limitOnly } = item; if (path && !limitOnly) { const routeProps = Object.assign({ debounceRoute, key: menuRoutes.length + 1, fallback: React.createElement(FallBack, null) }, item); routeProps.path = `${pathPrefix}${path}`; menuRoutes.push(React.createElement(PrestRoute, Object.assign({}, routeProps))); } }); return (React.createElement(Switch, null, menuRoutes, React.createElement(NotFoundRoute, null))); }; /** * 将routes配置转为路由的hooks,可用于更加方便创建自定义 layout */ export function useRoutesConfig(options) { const { routes, pathPrefix } = options; const { AuthRoutes, asideMenus = [], authRoutes = [] } = useMemo(() => { clearRouteStore(); setRoutesConfig(routes); const configs = { authRoutes: getAuthRoutes(), asideMenus: flattenTree(getAsideMenus()) .filter((i) => !!i.path) .map((i) => { i.path = `${pathPrefix || ''}${i.path}`; return i; }), }; return Object.assign(Object.assign({}, configs), { AuthRoutes: (React.createElement(AppMenuRoutes, { pathPrefix: pathPrefix, fallback: "loading...", authRoutes: configs.authRoutes })) }); }, [routes]); return { AuthRoutes, asideMenus, authRoutes }; }