react-router-decorator
Version:
基于 react-router-dom 封装的路由工具,提供装饰器/函数模式设置路由,自动排序,支持嵌套路由
385 lines (384 loc) • 16.2 kB
JavaScript
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);
};