inula-router问题修复及优化

This commit is contained in:
huangxuan 2023-12-01 15:43:36 +08:00
parent 1af8f0271a
commit 1f0dbaa180
No known key found for this signature in database
GPG Key ID: E79F50C67022565D
8 changed files with 58 additions and 27 deletions

View File

@ -16,7 +16,7 @@
import Inula from 'openinula'; import Inula from 'openinula';
import { useLayoutEffect, useRef, reduxAdapter, InulaNode } from 'openinula'; import { useLayoutEffect, useRef, reduxAdapter, InulaNode } from 'openinula';
import { connect, ReactReduxContext } from 'react-redux'; import { connect, ReactReduxContext } from 'react-redux';
import { Store } from 'redux'; import type { Store } from 'redux';
import { History, Location, Router } from '../router'; import { History, Location, Router } from '../router';
import { Action, DefaultStateType, Navigation } from '../history/types'; import { Action, DefaultStateType, Navigation } from '../history/types';
import { ActionMessage, onLocationChanged } from './actions'; import { ActionMessage, onLocationChanged } from './actions';
@ -130,9 +130,14 @@ function getConnectedRouter(type: StoreType) {
); );
}; };
const ConnectedHRouterWithContext = (props: any) => {
const { store, ...rest } = props;
return <ConnectedRouter store={store} storeType={type} {...rest} />;
};
// 针对不同的Store类型使用对应的connect函数 // 针对不同的Store类型使用对应的connect函数
if (type === 'InulaXCompat') { if (type === 'InulaXCompat') {
return hConnect(null as any, mapDispatchToProps)(ConnectedRouterWithContext as any); return hConnect(null as any, mapDispatchToProps)(ConnectedHRouterWithContext as any);
} }
if (type === 'Redux') { if (type === 'Redux') {
return connect(null, mapDispatchToProps)(ConnectedRouterWithContext); return connect(null, mapDispatchToProps)(ConnectedRouterWithContext);

View File

@ -107,7 +107,7 @@ export function createHashHistory<S = DefaultStateType>(option: HashHistoryOptio
warning(state !== undefined, 'Hash history does not support state, it will be ignored'); warning(state !== undefined, 'Hash history does not support state, it will be ignored');
const action = Action.push; const action = Action.push;
const location = createLocation<S>(history.location, to, undefined, ''); const location = createLocation<S>(history.location, to, state, '');
transitionManager.confirmJumpTo(location, action, getUserConfirmation, isJump => { transitionManager.confirmJumpTo(location, action, getUserConfirmation, isJump => {
if (!isJump) { if (!isJump) {
@ -132,7 +132,7 @@ export function createHashHistory<S = DefaultStateType>(option: HashHistoryOptio
function replace(to: To, state?: S) { function replace(to: To, state?: S) {
warning(state !== undefined, 'Hash history does not support state, it will be ignored'); warning(state !== undefined, 'Hash history does not support state, it will be ignored');
const action = Action.replace; const action = Action.replace;
const location = createLocation<S>(history.location, to, undefined, ''); const location = createLocation<S>(history.location, to, state, '');
transitionManager.confirmJumpTo(location, action, getUserConfirmation, isJump => { transitionManager.confirmJumpTo(location, action, getUserConfirmation, isJump => {
if (!isJump) { if (!isJump) {

View File

@ -28,8 +28,8 @@ export function createPath(path: Partial<Path>): string {
} }
export function parsePath(url: string): Partial<Path> { export function parsePath(url: string): Partial<Path> {
let pathname = url || '/';
const parsedPath: Partial<Path> = { const parsedPath: Partial<Path> = {
pathname: url || '/',
search: '', search: '',
hash: '', hash: '',
}; };
@ -38,16 +38,16 @@ export function parsePath(url: string): Partial<Path> {
if (hashIdx > -1) { if (hashIdx > -1) {
const hash = url.substring(hashIdx); const hash = url.substring(hashIdx);
parsedPath.hash = hash === '#' ? '' : hash; parsedPath.hash = hash === '#' ? '' : hash;
url = url.substring(0, hashIdx); pathname = pathname.substring(0, hashIdx);
} }
const searchIdx = url.indexOf('?'); const searchIdx = url.indexOf('?');
if (searchIdx > -1) { if (searchIdx > -1) {
const search = url.substring(searchIdx); const search = url.substring(searchIdx);
parsedPath.search = search === '?' ? '' : search; parsedPath.search = search === '?' ? '' : search;
url = url.substring(0, searchIdx); pathname = pathname.substring(0, searchIdx);
} }
parsedPath.pathname = url; parsedPath.pathname = pathname;
return parsedPath; return parsedPath;
} }
@ -116,12 +116,12 @@ export function stripBasename(path: string, prefix: string): string {
export function createMemoryRecord<T, S>(initVal: S, fn: (arg: S) => T) { export function createMemoryRecord<T, S>(initVal: S, fn: (arg: S) => T) {
let visitedRecord: T[] = [fn(initVal)]; let visitedRecord: T[] = [fn(initVal)];
function getDelta(to: S, form: S): number { function getDelta(toKey: S, formKey: S): number {
let toIdx = visitedRecord.lastIndexOf(fn(to)); let toIdx = visitedRecord.lastIndexOf(fn(toKey));
if (toIdx === -1) { if (toIdx === -1) {
toIdx = 0; toIdx = 0;
} }
let fromIdx = visitedRecord.lastIndexOf(fn(form)); let fromIdx = visitedRecord.lastIndexOf(fn(formKey));
if (fromIdx === -1) { if (fromIdx === -1) {
fromIdx = 0; fromIdx = 0;
} }

View File

@ -24,27 +24,42 @@ import { parsePath } from '../history/utils';
type NavLinkProps = { type NavLinkProps = {
to: Partial<Location> | string | ((location: Location) => string | Partial<Location>); to: Partial<Location> | string | ((location: Location) => string | Partial<Location>);
isActive?: (match: Matched | null, location: Location) => boolean; isActive?<P extends { [K in keyof P]?: string }>(match: Matched<P> | null, location: Location): boolean;
exact?: boolean;
strict?: boolean;
sensitive?: boolean;
className?: string | ((isActive: boolean) => string);
activeClassName?: string;
[key: string]: any; [key: string]: any;
} & LinkProps; } & Omit<LinkProps, 'className'>;
type Page = 'page'; type Page = 'page';
function NavLink<P extends NavLinkProps>(props: P) { function NavLink<P extends NavLinkProps>(props: P) {
const { to, isActive, ...rest } = props; const { to, isActive, exact, strict, sensitive, className, activeClassName, ...rest } = props;
const context = useContext(Context); const context = useContext(Context);
const toLocation = typeof to === 'function' ? to(context.location) : to; const toLocation = typeof to === 'function' ? to(context.location) : to;
const { pathname } = typeof toLocation === 'string' ? parsePath(toLocation) : toLocation; const { pathname } = typeof toLocation === 'string' ? parsePath(toLocation) : toLocation;
const match = pathname ? matchPath(context.location.pathname, pathname) : null; const match = pathname ? matchPath(context.location.pathname, pathname, {
exact: exact,
strictMode: strict,
caseSensitive: sensitive,
}) : null;
const isLinkActive = match && isActive ? isActive(match, context.location) : false; const isLinkActive = !!(isActive ? isActive(match, context.location) : match);
let classNames = typeof className === 'function' ? className(isLinkActive) : className;
if (isLinkActive) {
classNames = [activeClassName, classNames].filter(Boolean).join('');
}
const page: Page = 'page'; const page: Page = 'page';
const otherProps = { const otherProps = {
'aria-current': isLinkActive ? page : false, className: classNames,
'aria-current': isLinkActive ? page : undefined,
...rest, ...rest,
}; };

View File

@ -43,15 +43,19 @@ export type RouteProps<P extends Record<string, any> = {}, Path extends string =
function Route<Path extends string, P extends Record<string, any> = GetURLParams<Path>>(props: RouteProps<P, Path>) { function Route<Path extends string, P extends Record<string, any> = GetURLParams<Path>>(props: RouteProps<P, Path>) {
const context = useContext(RouterContext); const context = useContext(RouterContext);
const { computed, location, path } = props; const { computed, location, path, component, render, strict, sensitive, exact } = props;
let { children, component, render } = props; let { children } = props;
let match: Matched<P> | null; let match: Matched<P> | null;
const routeLocation = location || context.location; const routeLocation = location || context.location;
if (computed) { if (computed) {
match = computed; match = computed;
} else if (path) { } else if (path) {
match = matchPath<P>(routeLocation.pathname, path); match = matchPath<P>(routeLocation.pathname, path, {
strictMode: strict,
caseSensitive: sensitive,
exact: exact,
});
} else { } else {
match = context.match; match = context.match;
} }

View File

@ -29,22 +29,27 @@ function Router<P extends RouterProps>(props: P) {
const { history, children = null } = props; const { history, children = null } = props;
const [location, setLocation] = useState(props.history.location); const [location, setLocation] = useState(props.history.location);
const pendingLocation = useRef<Location | null>(null); const pendingLocation = useRef<Location | null>(null);
const unListen = useRef<null | (() => void)>(null);
const isMount = useRef<boolean>(false);
// 在Router加载时就监听history地址变化以保证在始渲染时重定向能正确触发 // 在Router加载时就监听history地址变化以保证在始渲染时重定向能正确触发
const unListen = useRef<null | (() => void)>( if (unListen.current === null) {
history.listen(arg => { unListen.current = history.listen(arg => {
pendingLocation.current = arg.location; pendingLocation.current = arg.location;
}), });
); }
// 模拟componentDidMount和componentWillUnmount // 模拟componentDidMount和componentWillUnmount
useLayoutEffect(() => { useLayoutEffect(() => {
isMount.current = true;
if (unListen.current) { if (unListen.current) {
unListen.current(); unListen.current();
} }
// 监听history中的位置变化 // 监听history中的位置变化
unListen.current = history.listen(arg => { unListen.current = history.listen(arg => {
if (isMount.current) {
setLocation(arg.location); setLocation(arg.location);
}
}); });
if (pendingLocation.current) { if (pendingLocation.current) {
@ -53,6 +58,7 @@ function Router<P extends RouterProps>(props: P) {
return () => { return () => {
if (unListen.current) { if (unListen.current) {
isMount.current = false;
unListen.current(); unListen.current();
unListen.current = null; unListen.current = null;
pendingLocation.current = null; pendingLocation.current = null;

View File

@ -40,7 +40,7 @@ export type Matched<P = any> = {
const defaultOption: Required<ParserOption> = { const defaultOption: Required<ParserOption> = {
// url匹配时是否大小写敏感 // url匹配时是否大小写敏感
caseSensitive: true, caseSensitive: false,
// 是否严格匹配url结尾的/ // 是否严格匹配url结尾的/
strictMode: false, strictMode: false,
// 是否完全精确匹配 // 是否完全精确匹配

View File

@ -20,10 +20,11 @@ import RouterContext from './context';
function withRouter<C extends ComponentType>(Component: C) { function withRouter<C extends ComponentType>(Component: C) {
function ComponentWithRouterProp(props: any) { function ComponentWithRouterProp(props: any) {
const { wrappedComponentRef, ...rest } = props;
const { history, location, match } = useContext(RouterContext); const { history, location, match } = useContext(RouterContext);
const routeProps = { history: history, location: location, match: match }; const routeProps = { history: history, location: location, match: match };
return <Component {...props} {...routeProps} />; return <Component {...routeProps} {...rest} ref={wrappedComponentRef} />;
} }
return ComponentWithRouterProp; return ComponentWithRouterProp;