UNPKG

react-router-decorator

Version:

基于 react-router-dom 封装的路由工具,提供装饰器/函数模式设置路由,自动排序,支持嵌套路由

385 lines (384 loc) 16.2 kB
var __assign = (this && this.__assign) || function () { __assign = Object.assign || function(t) { for (var s, i = 1, n = arguments.length; i < n; i++) { s = arguments[i]; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p]; } return t; }; return __assign.apply(this, arguments); }; 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; }; var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) { if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { if (ar || !(i in from)) { if (!ar) ar = Array.prototype.slice.call(from, 0, i); ar[i] = from[i]; } } return to.concat(ar || Array.prototype.slice.call(from)); }; import { parse } from 'qs'; import * as React from 'react'; import * as ReactDOM from 'react-dom'; import { BrowserRouter, HashRouter, Link, Outlet, RouterProvider, createBrowserRouter, createHashRouter, useLocation, useMatch, useNavigate, useParams, useRoutes, } from 'react-router-dom'; export * from 'react-router-dom'; /** * 记录路由 */ var RoutesRecord = {}; var RouteWatcher = /** @class */ (function () { function RouteWatcher() { var _this = this; /** * 发布路由变化 */ this.publish = function () { _this.timeout && clearTimeout(_this.timeout); _this.timeout = setTimeout(function () { _this.subscribers.forEach(function (fn) { fn(); }); }, 10); }; /** * 订阅路由变化 * @param fn */ this.subscribe = function (fn) { _this.subscribers.push(fn); }; this.subscribers = []; } return RouteWatcher; }()); var watcher = new RouteWatcher(); var useWatcher = function () { var _a = React.useState(Object.keys(RoutesRecord).length), count = _a[0], setCount = _a[1]; React.useMemo(function () { watcher.subscribe(function () { return setCount(function (c) { return c + 1; }); }); }, []); return count; }; /** * 将地址栏 search 转换为 query 对象 */ export var transSearch2Query = function (search, options) { if (search === void 0) { search = ''; } return parse(search.replace(/^\?/, ''), options); }; /** * hook 获取地址栏中的 query 对象 */ export var useSearchQuery = function (options) { var _a = useLocation().search, search = _a === void 0 ? '?' : _a; return React.useMemo(function () { return transSearch2Query(location.search, options); }, [search]); }; /** * 获取所有的路由绝对路径及相关配置 */ export var getRouteConfig = function () { return Object.freeze(Object.keys(RoutesRecord) .filter(function (path) { if (path === void 0) { path = '/'; } return !path.endsWith('::') && !path.endsWith('*'); }) .reduce(function (prev, current) { prev[current] = __assign({}, RoutesRecord[current]); return prev; }, {})); }; /** * 路由辅助工具,可渲染关键路由,方便开发人员切换页面 */ export var DevRouterHelper = function (props) { var _a = props.label, label = _a === void 0 ? '快捷路由 (仅开发模式使用,鼠标移出消失、悬停上边界显示) :' : _a; var _b = React.useState(true), visible = _b[0], setVisible = _b[1]; var timeoutRef = React.useRef(); var show = React.useCallback(function () { timeoutRef.current && clearTimeout(timeoutRef.current); timeoutRef.current = setTimeout(function () { setVisible(true); }, 500); }, []); var hide = React.useCallback(function () { timeoutRef.current && clearTimeout(timeoutRef.current); setVisible(false); }, []); React.useEffect(function () { timeoutRef.current && clearTimeout(timeoutRef.current); timeoutRef.current = setTimeout(hide, 10000); return function () { return timeoutRef.current && clearTimeout(timeoutRef.current); }; }, [useWatcher()]); var routers = React.useMemo(function () { return Object.entries(getRouteConfig()).map(function (_a) { var path = _a[0], config = _a[1]; return (React.createElement(Link, { key: path, to: path, style: { marginRight: 12, fontSize: 12, color: '#428df5' } }, typeof config.title === 'function' ? config.title({}, {}) : config.title)); }); }, []); return (React.createElement("div", { style: { position: 'fixed', top: 0, left: 0, right: 0, minHeight: 20, zIndex: 99999999, }, onMouseEnter: show, onMouseLeave: hide }, React.createElement("style", null), React.createElement("div", { style: { padding: '6px 12px', background: '#dfdfdf', display: 'flex', justifyContent: 'flex-start', flexWrap: 'wrap', alignItems: 'center', fontSize: 12, lineHeight: '20px', opacity: visible ? 1 : 0, transform: visible ? 'translateY(0)' : 'translateY(-100%)', transition: 'all 0.3s ease-in-out', } }, React.createElement("span", { style: { color: 'red', marginRight: 12, userSelect: 'none' } }, label), routers))); }; /** * 默认页面 Wrapper 附加自动解析 params、query 追加 navigate * @param props * @constructor */ export var PageWrapper = function (props) { var Component = props.Component, title = props.title, childrenAsOutlet = props.childrenAsOutlet, _a = props.context, context = _a === void 0 ? '' : _a, path = props.path, lazy = props.lazy; var _b = useLocation(), _c = _b.search, search = _c === void 0 ? '?' : _c, pathname = _b.pathname; var params = useParams(); var navigate = useNavigate(); var query = React.useMemo(function () { return transSearch2Query(search); }, [search]); var fullPath = getAbsolutePath(context, path); // 判断路由是否匹配 var matched = !!useMatch(fullPath); // 设置 document.title React.useEffect(function () { var originalTitle = document.title; if (matched && title) { document.title = typeof title === 'function' ? title(params, query) : title; } return function () { document.title = originalTitle; }; }, [title, pathname, search, matched]); var Wrapper = lazy ? React.Suspense : React.Fragment; return (React.createElement(Wrapper, null, React.createElement(Component, { path: fullPath, params: params, query: query, navigate: navigate }, !!childrenAsOutlet && React.createElement(Outlet, null)))); }; /** * 路由路径统一转换为绝对路径 * @param paths */ var getAbsolutePath = function () { var paths = []; for (var _i = 0; _i < arguments.length; _i++) { paths[_i] = arguments[_i]; } return __spreadArray(['/'], paths.filter(function (path) { return Boolean(path === null || path === void 0 ? void 0 : path.trim()); }).map(function (path) { return path.trim(); }), true).join('/').replace(/\/+/g, '/') || '/'; }; /** * 路由路径正则 */ var routeRegExp = /^(\/?((:?[\w-]+\??)|(\*)))+$/i; var legalRouteRules = "/\ntest\n/test\n/:id\n/:id?\n/:name/:id\n/:name/:id?\n/:name?/:id?\n/test/*\n/:name/*\n/:name?/*\n/*\n*" .split('\n') .map(function (str) { return str.trim(); }); // legalRouteRules.map((str) => console[routeRegExp.test(str) ? 'log' : 'error'](str)); /** * 类装饰器,用于注册类组件路由 * @param path 路由路径 * @param options 附加参数 */ export var page = function (path, options) { var _a = typeof options === 'string' ? { title: options } : options !== null && options !== void 0 ? options : {}, _b = _a.context, context = _b === void 0 ? '' : _b, _c = _a.index, index = _c === void 0 ? false : _c, others = __rest(_a, ["context", "index"]); // 嵌套路由上下文不合法 if (context.trim() && !context.startsWith('/')) { throw new Error('嵌套路由上下文不合法,请使用 "/" 开始的绝对路径,多级嵌套使用完整上下文'); } var absolutePath = getAbsolutePath(path); if (absolutePath !== '/' && !routeRegExp.test(path)) { throw new Error("\u8DEF\u7531\u8DEF\u5F84\u4E0D\u5408\u6CD5\uFF0C\u652F\u6301\u7684\u8DEF\u7531\u8DEF\u5F84\u5982: ".concat(legalRouteRules.map(function (p) { return "\"".concat(p, "\""); }))); } var key = getAbsolutePath(context, index ? '' : path); return function (Component) { // 如果是 index 路由,则路径会和父路径相同,增加 :: 标记防止 key 重复 RoutesRecord[index ? "".concat(key, "::") : key] = __assign(__assign({}, others), { path: absolutePath, index: index, Component: Component, context: context }); watcher.publish(); }; }; /** * 函数注册路由,类组件和函数组件均可使用 * @param Component * @param path * @param options */ export var $page = function (Component, path, options) { return page(path, options)(Component); }; /** * 将路由配置转换为目标对象 * @param config * @param options */ var transRoute = function (config, options) { var _a, _b, _c; var path = config.path, Component = config.Component, title = config.title, context = config.context, lazy = config.lazy, index = config.index, routeObject = __rest(config, ["path", "Component", "title", "context", "lazy", "index"]); var _withPageWrapper = (_a = Component.withPageWrapper) !== null && _a !== void 0 ? _a : options.withPageWrapper; var _childrenAsOutlet = (_b = Component.childrenAsOutlet) !== null && _b !== void 0 ? _b : options.childrenAsOutlet; var _PageWrapper = (_c = options.PageWrapper) !== null && _c !== void 0 ? _c : PageWrapper; var Wrapper = lazy ? React.Suspense : React.Fragment; return __assign(__assign({}, routeObject), { index: index, path: index ? '' : path, element: _withPageWrapper ? (React.createElement(_PageWrapper, { context: context, path: path, Component: Component, title: title, childrenAsOutlet: _childrenAsOutlet, lazy: lazy })) : (React.createElement(Wrapper, null, React.createElement(Component, null, _childrenAsOutlet && React.createElement(Outlet, null)))) }); }; /** * 路由路径排序算法 * @param a * @param b */ export var routeSorter = function (a, b) { var aArr = a.split('/'); var bArr = b.split('/'); var len = Math.max(aArr.length, bArr.length); for (var i = 0; i < len; i++) { if (aArr[i] && !bArr[i]) return 1; if (!aArr[i] && bArr[i]) return -1; if (aArr[i] === bArr[i]) { continue; } // *优先级最低 if (aArr[i] === '*') return 1; if (bArr[i] === '*') return -1; return aArr[i] > bArr[i] ? 1 : -1; } return a > b ? 1 : -1; }; /** * 获取路由配置 * @param options */ export var getRoutes = function (options, dataRouter) { var _a = options.withPageWrapper, withPageWrapper = _a === void 0 ? true : _a, PageWrapper = options.PageWrapper, _b = options.childrenAsOutlet, childrenAsOutlet = _b === void 0 ? false : _b, helper = options.helper, _c = options.debug, debug = _c === void 0 ? false : _c; var routes = []; var root = RoutesRecord["/"], pages = __rest(RoutesRecord, ['/']); var deepContextRoute = function (ctx, routes, p) { var len = routes.length; var route; for (var i = len - 1; i >= 0; i--) { var _a = routes[i], path = _a.path, children = _a.children; var absolutePath = getAbsolutePath(p !== null && p !== void 0 ? p : '', path); if (!ctx.startsWith(absolutePath)) { continue; } if (absolutePath === ctx) { route = routes[i]; } if (!route && children) { route = deepContextRoute(ctx, children, absolutePath); } if (route) { break; } } return route; }; var getContextRoute = function (ctx) { var _ctx = routes.find(function (r) { return r.path === ctx; }); if (!_ctx) { return deepContextRoute(ctx, routes); } return _ctx; }; var transOption = { withPageWrapper: withPageWrapper, childrenAsOutlet: childrenAsOutlet, PageWrapper: PageWrapper }; root && routes.push(transRoute(root, transOption)); Object.keys(pages) .sort(routeSorter) .forEach(function (p) { var option = pages[p]; var path = option.path, context = option.context, config = __rest(option, ["path", "context"]); // 有上下文则是嵌套路由 if (context) { var parent_1 = getContextRoute(context); if (parent_1 && !parent_1.children) { parent_1.children = [transRoute(__assign({ path: path.substring(1), context: context }, config), transOption)]; } else if (parent_1 === null || parent_1 === void 0 ? void 0 : parent_1.children) { parent_1.children.push(transRoute(__assign({ path: path.substring(1), context: context }, config), transOption)); } else { console.error('未找到匹配的嵌套上下文'); } } else { routes.push(transRoute(option, transOption)); } }); debug && console.warn('[react-router-decorator debug] 路由配置如下:\n\n', routes); if (dataRouter) { return [ { path: '', children: routes, element: (React.createElement(React.Fragment, null, (helper !== null && helper !== void 0 ? helper : debug) && React.createElement(DevRouterHelper, null), React.createElement(Outlet, null))), }, ]; } return routes; }; /** * By use useRoutes([...]) */ export var AppRoutes = function (props) { return useRoutes(React.useMemo(function () { return getRoutes(props); }, [useWatcher()])); }; /** * Router with Routes do not support the data APIs. */ export var AppRouter = function (props) { var type = props.type, _a = props.Wrapper, Wrapper = _a === void 0 ? React.Fragment : _a, helper = props.helper, others = __rest(props, ["type", "Wrapper", "helper"]); var Router = type === 'history' ? BrowserRouter : HashRouter; return (React.createElement(Wrapper, null, React.createElement(Router, null, (helper !== null && helper !== void 0 ? helper : props.debug) && React.createElement(DevRouterHelper, null), React.createElement(AppRoutes, __assign({}, others))))); }; /** * Router with Routes and support the Data APIs. */ export var DataRouter = function (props) { var type = props.type, _a = props.Wrapper, Wrapper = _a === void 0 ? React.Fragment : _a; var createRouter = type === 'history' ? createBrowserRouter : createHashRouter; var router = React.useMemo(function () { return createRouter(getRoutes(props, true)); }, [Object.keys(RoutesRecord).join(';')]); return (React.createElement(Wrapper, null, React.createElement(RouterProvider, { router: router }))); }; /** * By ReactDOM.render * @param element * @param options * @deprecated when use React 18. */ export var renderApp = function (element, options) { ReactDOM.render(React.createElement(AppRouter, __assign({}, options)), element); };