Match-id-5ae8e357b37201f329c6f95c7d89b45208d7084f

This commit is contained in:
* 2023-09-01 09:11:54 +08:00
parent 8c3f54123c
commit c558290682
124 changed files with 10641 additions and 0 deletions

View File

@ -0,0 +1,32 @@
/*
* Copyright (c) 2020 Huawei Technologies Co.,Ltd.
*
* openGauss is licensed under Mulan PSL v2.
* You can use this software according to the terms and conditions of the Mulan PSL v2.
* You may obtain a copy of Mulan PSL v2 at:
*
* http://license.coscl.org.cn/MulanPSL2
*
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
* See the Mulan PSL v2 for more details.
*/
'use strict';
module.exports = {
printWidth: 120, //一行120字符数如果超过会进行换行
tabWidth: 2, //tab等2个空格
useTabs: false, //用空格缩进行
semi: true, //行尾使用分号
singleQuote: true, //字符串使用单引号
quoteProps: 'as-needed', //仅在需要时在对象属性添加引号
jsxSingleQuote: false, //在JSX中使用双引号
trailingComma: 'es5', //使用尾逗号(对象、数组等)
bracketSpacing: true, //对象的括号间增加空格
bracketSameLine: false, //将多行JSX元素的>放在最后一行的末尾
arrowParens: 'avoid', //在唯一的arrow函数参数周围省略括号
vueIndentScriptAndStyle: false, //不缩进Vue文件中的<script>和<style>标记内的代码
endOfLine: 'lf', //仅限换行(\n
};

View File

@ -0,0 +1,109 @@
# Horizon-router
Horizon-router 是Horizon生态组建的一部分为Horizon提供前端路由的能力是构建大型应用必要组件。
Horizon-router涵盖react-router、history、connect-react-router的功能。
## 从react-router切换
Horizon-router 在API设计上兼容react-router V5。
在切换时只需在`package.json`的dependencies中加入`horizon-router`
并将原有的API引用从`react-router-dom`、`history`、`connected-react-router
`切换到`horizon-router`。
切换样例代码如下:
### react-router-dom
**切换前**
```typescript
import { Switch, Route } from 'react-router-dom';
```
**切换后**
```typescript
import { Switch, Route } from 'horizon-router';
```
### history
**切换前**
```typescript
import { createHashHistory, createBrowserHistory } from 'history';
```
**切换后**
```typescript
import { createHashHistory, createBrowserHistory } from 'horizon-router';
```
### connected-react-router
**切换前**
```typescript
import { ConnectedRouter, routerMiddleware, connectRouter } from 'connected-react-router';
```
**切换后**
```typescript
import { ConnectedRouter, routerMiddleware, connectRouter } from 'horizon-router';
```
## Horizon-router API列表
history 兼容API
---
- createBrowserHistory
- createHashHistory
react-router-dom 兼容API
---
- __RouterContext
- matchPath
- generatePath
- useHistory
- useLocation
- useParams
- useRouteMatch
- Route
- Router
- Switch
- Redirect
- Prompt
- withRouter
- HashRouter
- BrowserRouter
- Link
- NavLink
react-router-dom 类型兼容API
---
- RouteComponentProps
- RouteChildrenProps
- RouteProps
connected-react-router 兼容API
---
- connectRouter
- routerMiddleware
- ConnectedRouter
connected-react-router 新增API
---
- ConnectedHRouter(在HorizonX的Redux兼容模式中使用)
## 问题反馈
Horizon-router问题与bug反馈请联系00800104 黄轩

View File

@ -0,0 +1,49 @@
/*
* Copyright (c) 2020 Huawei Technologies Co.,Ltd.
*
* openGauss is licensed under Mulan PSL v2.
* You can use this software according to the terms and conditions of the Mulan PSL v2.
* You may obtain a copy of Mulan PSL v2 at:
*
* http://license.coscl.org.cn/MulanPSL2
*
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
* See the Mulan PSL v2 for more details.
*/
export default {
presets: ['@babel/preset-typescript', ['@babel/preset-env', { targets: { node: 'current' } }]],
plugins: [
'@babel/plugin-syntax-jsx',
[
'@babel/plugin-transform-react-jsx',
{
pragma: 'React.createElement',
pragmaFrag: 'React.Fragment',
},
],
['@babel/plugin-proposal-class-properties', { loose: true }],
['@babel/plugin-proposal-private-methods', { loose: true }],
['@babel/plugin-proposal-private-property-in-object', { loose: true }],
'@babel/plugin-transform-object-assign',
'@babel/plugin-transform-object-super',
['@babel/plugin-proposal-object-rest-spread', { loose: true, useBuiltIns: true }],
['@babel/plugin-transform-template-literals', { loose: true }],
'@babel/plugin-transform-arrow-functions',
'@babel/plugin-transform-literals',
'@babel/plugin-transform-for-of',
'@babel/plugin-transform-block-scoped-functions',
'@babel/plugin-transform-classes',
'@babel/plugin-transform-shorthand-properties',
'@babel/plugin-transform-computed-properties',
'@babel/plugin-transform-parameters',
['@babel/plugin-transform-spread', { loose: true, useBuiltIns: true }],
['@babel/plugin-transform-block-scoping', { throwIfClosureRequired: false }],
['@babel/plugin-transform-destructuring', { loose: true, useBuiltIns: true }],
'@babel/plugin-transform-runtime',
'@babel/plugin-proposal-nullish-coalescing-operator',
'@babel/plugin-proposal-optional-chaining',
],
};

View File

@ -0,0 +1,100 @@
import path from 'path';
import { fileURLToPath } from 'url';
import babel from '@rollup/plugin-babel';
import nodeResolve from '@rollup/plugin-node-resolve';
import execute from 'rollup-plugin-execute';
import fs from 'fs';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const routerEntry = path.join(__dirname, '/src/router/index.ts');
const connectRouterEntry = path.join(__dirname, '/src/router/index2.ts');
const output = __dirname;
const extensions = ['.js', '.ts', '.tsx'];
if (!fs.existsSync(path.join(output, 'connectRouter'))) {
fs.mkdirSync(path.join(output, 'connectRouter'), { recursive: true });
}
const routerBuildConfig = {
input: { router: routerEntry },
output: [
{
dir: path.resolve(output, 'router/cjs'),
sourcemap: 'inline',
format: 'cjs',
},
{
dir: path.resolve(output, 'router/esm'),
sourcemap: 'inline',
format: 'esm',
},
],
plugins: [
nodeResolve({
extensions,
modulesOnly: true,
}),
babel({
exclude: 'node_modules/**',
configFile: path.join(__dirname, '/babel.config.js'),
babelHelpers: 'runtime',
extensions,
}),
execute('npm run build-types-router'),
],
};
const connectRouterConfig = {
input: { connectRouter: connectRouterEntry },
output: [
{
dir: path.resolve(output, 'connectRouter/cjs'),
sourcemap: 'inline',
format: 'cjs',
},
{
dir: path.resolve(output, 'connectRouter/esm'),
sourcemap: 'inline',
format: 'esm',
},
],
plugins: [
nodeResolve({
extensions,
modulesOnly: true,
}),
babel({
exclude: 'node_modules/**',
configFile: path.join(__dirname, '/babel.config.js'),
babelHelpers: 'runtime',
extensions,
}),
execute('npm run build-types-all'),
copyFiles([
{
from: path.join(__dirname, 'src/configs/package.json'),
to: path.join(output, '/connectRouter/package.json'),
},
]),
],
};
function copyFiles(copyPairs) {
return {
name: 'copy-files',
generateBundle() {
copyPairs.forEach(({ from, to }) => {
console.log(`copy files: ${from}${to}`);
fs.copyFileSync(from, to);
});
},
};
}
export default [routerBuildConfig, connectRouterConfig];

View File

@ -0,0 +1,25 @@
import { Action, Path } from '../history/types';
type Location = Partial<Path>;
export declare enum ActionName {
LOCATION_CHANGE = "$horizon-router/LOCATION_CHANGE",
CALL_HISTORY_METHOD = "$horizon-router/CALL_HISTORY_METHOD"
}
export type ActionMessage = {
type: ActionName.LOCATION_CHANGE;
payload: {
location: Location;
action: Action;
isFirstRendering: boolean;
};
} | {
type: ActionName.CALL_HISTORY_METHOD;
payload: {
method: string;
args: any;
};
};
export declare const onLocationChanged: (location: Location, action: Action, isFirstRendering?: boolean) => ActionMessage;
export declare const push: (...args: any) => ActionMessage;
export declare const replace: (...args: any) => ActionMessage;
export declare const go: (...args: any) => ActionMessage;
export {};

View File

@ -0,0 +1,3 @@
import { ActionMessage } from './actions';
import { History } from '../history/types';
export declare function routerMiddleware(history: History): (_: any) => (next: any) => (action: ActionMessage) => any;

View File

@ -0,0 +1,11 @@
export { getConnectedRouter } from './connectedRouter';
export declare const connectRouter: (history: import("../router").History<unknown>) => (state?: {
location: Partial<import("../router").Location<unknown>> & {
query?: Record<string, any>;
};
action: import("../history/types").Action;
}, { type, payload }?: {
type?: import("./actions").ActionName;
payload?: any;
}) => any;
export { routerMiddleware } from './dispatch';

View File

@ -0,0 +1,16 @@
import { ActionName } from './actions';
import { Action, History } from '../history/types';
import { Location } from '../router';
type LocationWithQuery = Partial<Location> & {
query?: Record<string, any>;
};
type InitRouterState = {
location: LocationWithQuery;
action: Action;
};
type Payload = {
type?: ActionName;
payload?: any;
};
export declare function createConnectRouter(): (history: History) => (state?: InitRouterState, { type, payload }?: Payload) => any;
export {};

View File

@ -0,0 +1,10 @@
import { HistoryProps, Listener, Navigation, Prompt } from './types';
import transitionManager from './transitionManager';
export declare function getBaseHistory<S>(transitionManager: transitionManager<S>, setListener: (delta: number) => void, browserHistory: History): {
go: (step: number) => void;
goBack: () => void;
goForward: () => void;
listen: (listener: Listener<S>) => () => void;
block: (prompt?: Prompt<S>) => () => void;
getUpdateStateFunc: (historyProps: HistoryProps<S>) => (nextState: Navigation<S> | undefined) => void;
};

View File

@ -0,0 +1,8 @@
import { BaseOption, DefaultStateType, History } from './types';
export type BrowserHistoryOption = {
/**
* forceRefresh为True时跳转时会强制刷新页面
*/
forceRefresh?: boolean;
} & BaseOption;
export declare function createBrowserHistory<S = DefaultStateType>(options?: BrowserHistoryOption): History<S>;

View File

@ -0,0 +1,4 @@
export declare function isBrowser(): boolean;
export declare function getDefaultConfirmation(message: string, callBack: (result: boolean) => void): void;
export declare function isSupportHistory(): boolean;
export declare function isSupportsPopState(): boolean;

View File

@ -0,0 +1,7 @@
import { BaseOption, DefaultStateType, History } from './types';
export type urlHashType = 'slash' | 'noslash';
type HashHistoryOption = {
hashType?: urlHashType;
} & BaseOption;
export declare function createHashHistory<S = DefaultStateType>(option?: HashHistoryOption): History<S>;
export {};

View File

@ -0,0 +1,11 @@
import { Action, CallBackFunc, ConfirmationFunc, Listener, Location, Navigation, Prompt, TManager } from './types';
declare class TransitionManager<S> implements TManager<S> {
private prompt;
private listeners;
constructor();
setPrompt(prompt: Prompt<S>): () => void;
addListener(func: Listener<S>): () => void;
notifyListeners(args: Navigation<S>): void;
confirmJumpTo(location: Location<S>, action: Action, userConfirmationFunc: ConfirmationFunc, callBack: CallBackFunc): void;
}
export default TransitionManager;

View File

@ -0,0 +1,56 @@
export type BaseOption = {
basename?: string;
getUserConfirmation?: ConfirmationFunc;
};
export interface HistoryProps<T = unknown> {
readonly action: Action;
readonly location: Location<T>;
length: number;
}
export interface History<T = unknown> extends HistoryProps<T> {
createHref(path: Partial<Path>): string;
push(to: To, state?: T): void;
replace(to: To, state?: T): void;
listen(listener: Listener<T>): () => void;
block(prompt: Prompt<T>): () => void;
go(index: number): void;
goBack(): void;
goForward(): void;
}
export declare enum Action {
pop = "POP",
push = "PUSH",
replace = "REPLACE"
}
export declare enum EventType {
PopState = "popstate",
HashChange = "hashchange"
}
export type Path = {
pathname: string;
search: string;
hash: string;
};
export type HistoryState<T> = {
state?: T;
key: string;
};
export type DefaultStateType = unknown;
export type Location<T = unknown> = Path & HistoryState<T>;
export type To = string | Partial<Path>;
export interface Listener<T = unknown> {
(navigation: Navigation<T>): void;
}
export interface Navigation<T = unknown> {
action: Action;
location: Location<T>;
}
export type Prompt<S> = string | boolean | null | ((location: Location<S>, action: Action) => void);
export type CallBackFunc = (isJump: boolean) => void;
export type ConfirmationFunc = (message: string, callBack: CallBackFunc) => void;
export interface TManager<S> {
setPrompt(next: Prompt<S>): () => void;
addListener(func: (navigation: Navigation<S>) => void): () => void;
notifyListeners(args: Navigation<S>): void;
confirmJumpTo(location: Location<S>, action: Action, userConfirmationFunc: ConfirmationFunc, callBack: CallBackFunc): void;
}

View File

@ -0,0 +1,14 @@
import { Action, Location, Path, To } from './types';
export declare function createPath(path: Partial<Path>): string;
export declare function parsePath(url: string): Partial<Path>;
export declare function createLocation<S>(current: string | Location, to: To, state?: S, key?: string): Readonly<Location<S>>;
export declare function isLocationEqual(p1: Partial<Path>, p2: Partial<Path>): boolean;
export declare function addHeadSlash(path: string): string;
export declare function stripHeadSlash(path: string): string;
export declare function normalizeSlash(path: string): string;
export declare function hasBasename(path: string, prefix: string): Boolean;
export declare function stripBasename(path: string, prefix: string): string;
export declare function createMemoryRecord<T, S>(initVal: S, fn: (arg: S) => T): {
getDelta: (to: S, form: S) => number;
addRecord: (current: S, newRecord: S, action: Action) => void;
};

View File

@ -0,0 +1,2 @@
declare function warning(condition: any, message: string): void;
export default warning;

View File

@ -0,0 +1,12 @@
import { ReactNode } from 'react';
import { ConfirmationFunc } from '../history/types';
export type BaseRouterProps = {
basename: string;
getUserConfirmation: ConfirmationFunc;
children?: ReactNode;
};
export type BrowserRouterProps = BaseRouterProps & {
forceRefresh: boolean;
};
declare function BrowserRouter<P extends Partial<BrowserRouterProps>>(props: P): JSX.Element;
export default BrowserRouter;

View File

@ -0,0 +1,7 @@
import { BaseRouterProps } from './BrowserRouter';
import { urlHashType } from '../history/hashHistory';
export type HashRouterProps = BaseRouterProps & {
hashType: urlHashType;
};
declare function HashRouter<P extends Partial<HashRouterProps>>(props: P): JSX.Element;
export default HashRouter;

View File

@ -0,0 +1,18 @@
import * as React from 'react';
import { Location } from './index';
export type LinkProps = {
component?: React.ComponentType<any>;
to: Partial<Location> | string | ((location: Location) => string | Partial<Location>);
replace?: boolean;
tag?: string;
/**
* @deprecated
* React16以后不再需要该属性
**/
innerRef?: React.Ref<HTMLAnchorElement>;
} & React.AnchorHTMLAttributes<HTMLAnchorElement>;
declare function Link<P extends LinkProps>(props: P): React.DOMElement<{
href: string;
onClick: (event: React.MouseEvent<HTMLAnchorElement>) => void;
} & Omit<P, "replace" | "to" | "component" | "onClick" | "target">, Element>;
export default Link;

View File

@ -0,0 +1,10 @@
import type { LinkProps } from './Link';
import { Location } from './index';
import { Matched } from './matcher/parser';
type NavLinkProps = {
to: Partial<Location> | string | ((location: Location) => string | Partial<Location>);
isActive?: (match: Matched | null, location: Location) => boolean;
[key: string]: any;
} & LinkProps;
declare function NavLink<P extends NavLinkProps>(props: P): JSX.Element;
export default NavLink;

View File

@ -0,0 +1,8 @@
import { Location } from './index';
import { Action } from '../history/types';
type PromptProps = {
message?: string | ((location: Partial<Location>, action: Action) => void);
when?: boolean | ((location: Partial<Location>) => boolean);
};
declare function Prompt<P extends PromptProps>(props: P): JSX.Element;
export default Prompt;

View File

@ -0,0 +1,13 @@
import { Matched } from './matcher/parser';
import { Location } from './index';
export type RedirectProps = {
to: string | Partial<Location>;
push?: boolean;
path?: string;
from?: string;
exact?: boolean;
strict?: boolean;
readonly computed?: Matched | null;
};
declare function Redirect<P extends RedirectProps>(props: P): JSX.Element;
export default Redirect;

View File

@ -0,0 +1,23 @@
import * as React from 'react';
import { History, Location } from './index';
import { Matched } from './matcher/parser';
import { GetURLParams } from './matcher/types';
export type RouteComponentProps<P extends Record<string, any> = {}, S = unknown> = RouteChildrenProps<P, S>;
export type RouteChildrenProps<P extends Record<string, any> = {}, S = unknown> = {
history: History<S>;
location: Location<S>;
match: Matched<P> | null;
};
export type RouteProps<P extends Record<string, any> = {}, Path extends string = string> = {
location?: Location;
component?: React.ComponentType<RouteComponentProps<P>> | React.ComponentType<any> | undefined;
children?: ((props: RouteChildrenProps<P>) => React.ReactNode) | React.ReactNode;
render?: (props: RouteComponentProps<P>) => React.ReactNode;
path?: Path | Path[];
exact?: boolean;
sensitive?: boolean;
strict?: boolean;
computed?: Matched<P>;
};
declare function Route<Path extends string, P extends Record<string, any> = GetURLParams<Path>>(props: RouteProps<P, Path>): JSX.Element;
export default Route;

View File

@ -0,0 +1,8 @@
import * as React from 'react';
import { History } from '../history/types';
export type RouterProps = {
history: History;
children?: React.ReactNode;
};
declare function Router<P extends RouterProps>(props: P): JSX.Element;
export default Router;

View File

@ -0,0 +1,8 @@
import * as React from 'react';
import { Location } from './index';
export type SwitchProps = {
location?: Location;
children?: React.ReactNode;
};
declare function Switch<P extends SwitchProps>(props: P): React.ReactElement | null;
export default Switch;

View File

@ -0,0 +1 @@
import '@testing-library/jest-dom';

View File

@ -0,0 +1,8 @@
import { History, Location } from '../index';
export declare let historyHook: History;
export declare let locationHook: Location;
export declare const LocationDisplay: () => JSX.Element;
export declare const Test_Demo: () => JSX.Element;
export declare const Test_Demo2: () => JSX.Element;
export declare const Test_Demo3: () => JSX.Element;
export declare const Test_Demo4: () => JSX.Element;

View File

@ -0,0 +1,10 @@
/// <reference types="react" />
import { History, Location } from './index';
import { Matched } from './matcher/parser';
export type RouterContextValue = {
history: History;
location: Location;
match: Matched | null;
};
declare const RouterContext: import("react").Context<RouterContextValue>;
export default RouterContext;

View File

@ -0,0 +1,8 @@
import { Matched, Params } from './matcher/parser';
import { History } from '../history/types';
import { Location } from './index';
declare function useHistory<S>(): History<S>;
declare function useLocation<S>(): Location<S>;
declare function useParams<P>(): Params<P> | {};
declare function useRouteMatch<P>(path?: string): Matched<P> | null;
export { useHistory, useLocation, useParams, useRouteMatch };

View File

@ -0,0 +1,20 @@
import { Location as HLocation } from '../history/types';
type Location<S = unknown> = Omit<HLocation<S>, 'key'>;
export { Location };
export type { History } from '../history/types';
export { createBrowserHistory } from '../history/browerHistory';
export { createHashHistory } from '../history/hashHistory';
export { default as __RouterContext } from './context';
export { matchPath, generatePath } from './matcher/parser';
export { useHistory, useLocation, useParams, useRouteMatch } from './hooks';
export { default as Route } from './Route';
export { default as Router } from './Router';
export { default as Switch } from './Switch';
export { default as Redirect } from './Redirect';
export { default as Prompt } from './Prompt';
export { default as withRouter } from './withRouter';
export { default as HashRouter } from './HashRouter';
export { default as BrowserRouter } from './BrowserRouter';
export { default as Link } from './Link';
export { default as NavLink } from './NavLink';
export type { RouteComponentProps, RouteChildrenProps, RouteProps } from './Route';

View File

@ -0,0 +1,23 @@
import { Location as HLocation } from '../history/types';
type Location<S = unknown> = Omit<HLocation<S>, 'key'>;
export { Location };
export type { History } from '../history/types';
export { createBrowserHistory } from '../history/browerHistory';
export { createHashHistory } from '../history/hashHistory';
export { default as __RouterContext } from './context';
export { matchPath, generatePath } from './matcher/parser';
export { useHistory, useLocation, useParams, useRouteMatch } from './hooks';
export { default as Route } from './Route';
export { default as Router } from './Router';
export { default as Switch } from './Switch';
export { default as Redirect } from './Redirect';
export { default as Prompt } from './Prompt';
export { default as withRouter } from './withRouter';
export { default as HashRouter } from './HashRouter';
export { default as BrowserRouter } from './BrowserRouter';
export { default as Link } from './Link';
export { default as NavLink } from './NavLink';
export type { RouteComponentProps, RouteChildrenProps, RouteProps } from './Route';
export { connectRouter, routerMiddleware } from '../connect-router';
export declare const ConnectedRouter: any;
export declare const ConnectedHRouter: any;

View File

@ -0,0 +1,7 @@
export type LifeCycleProps = {
onMount?: () => void;
onUpdate?: (prevProps?: LifeCycleProps) => void;
onUnmount?: () => void;
data?: any;
};
export declare function LifeCycle(props: LifeCycleProps): any;

View File

@ -0,0 +1 @@
export {};

View File

@ -0,0 +1 @@
export {};

View File

@ -0,0 +1 @@
export {};

View File

@ -0,0 +1,2 @@
import { Token } from './types';
export declare function lexer(path: string): Token[];

View File

@ -0,0 +1,18 @@
import { GetURLParams, Parser, ParserOption } from './types';
export type Params<P> = {
[K in keyof P]?: P[K];
};
export type Matched<P = any> = {
score: number[];
params: Params<P>;
path: string;
url: string;
isExact: boolean;
};
export declare function createPathParser<Str extends string>(pathname: Str, option?: ParserOption): Parser<GetURLParams<Str>>;
export declare function createPathParser<P = unknown>(pathname: string, option?: ParserOption): Parser<P>;
/**
* @description 使pathname与pattern进行匹配
*/
export declare function matchPath<P = any>(pathname: string, pattern: string | string[], option?: ParserOption): Matched<P> | null;
export declare function generatePath<P = any>(path: string, params: Params<P>): string;

View File

@ -0,0 +1,36 @@
import { Matched, Params } from './parser';
export type Token = {
type: TokenType;
value: string;
};
export declare enum TokenType {
Delimiter = "delimiter",
Static = "static",
Param = "param",
WildCard = "wildcard",
LBracket = "(",
RBracket = ")",
Pattern = "pattern"
}
export interface Parser<P> {
regexp: RegExp;
keys: string[];
parse(url: string): Matched<P> | null;
compile(params: Params<P>): string;
}
export type ParserOption = {
caseSensitive?: boolean;
strictMode?: boolean;
exact?: boolean;
};
type ClearLeading<U extends string> = U extends `/${infer R}` ? ClearLeading<R> : U;
type ClearTailing<U extends string> = U extends `${infer L}/` ? ClearTailing<L> : U;
type ParseParam<Param extends string> = Param extends `:${infer R}` ? {
[K in R]: string;
} : {};
type MergeParams<OneParam extends Record<string, any>, OtherParam extends Record<string, any>> = {
readonly [Key in keyof OneParam | keyof OtherParam]?: string;
};
type ParseURLString<Str extends string> = Str extends `${infer Param}/${infer Rest}` ? MergeParams<ParseParam<Param>, ParseURLString<ClearLeading<Rest>>> : ParseParam<Str>;
export type GetURLParams<U extends string> = ParseURLString<ClearLeading<ClearTailing<U>>>;
export {};

View File

@ -0,0 +1,6 @@
/**
* @description url中的//转换为/
*/
export declare function cleanPath(path: string): string;
export declare function scoreCompare(score1: number[], score2: number[]): number;
export declare function escapeStr(str: string): string;

View File

@ -0,0 +1,3 @@
import * as React from 'react';
declare function withRouter<C extends React.ComponentType>(Component: C): (props: any) => JSX.Element;
export default withRouter;

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,9 @@
{
"module": "./esm/connectRouter.js",
"main": "./cjs/connectRouter.js",
"types": "./@types/router/index2.d.ts",
"peerDependencies": {
"react-redux": "^6.0.0 || ^7.1.0",
"redux": "^3.6.0 || ^4.0.0"
}
}

View File

@ -0,0 +1,8 @@
export default {
preset: "ts-jest",
testMatch: ["**/__tests__/*.test.[jt]s?(x)"],
globals: {
__DEV__: true,
},
testEnvironment: 'jsdom'
};

View File

@ -0,0 +1,93 @@
{
"name": "@cloudsop/horizon-router",
"version": "0.0.12",
"description": "router for horizon framework, a part of horizon-ecosystem",
"main": "./router/cjs/router.js",
"module": "./router/esm/router.js",
"types": "./router/@types/router/index.d.ts",
"type": "module",
"files": [
"/connectRouter",
"/router",
"README.md"
],
"bugs": {
"url": "https://open.codehub.huawei.com/innersource/fenghuang/horizon/horizon-ecosystem/issues"
},
"scripts": {
"test": "jest",
"build": "rollup -c build.js",
"build-types-router": "tsc -p src/router/index.ts --emitDeclarationOnly --declaration --declarationDir ./router/@types --skipLibCheck",
"build-types-all": "tsc -p src/router/index2.ts --emitDeclarationOnly --declaration --declarationDir ./connectRouter/@types --skipLibCheck"
},
"repository": {
"type": "git",
"url": "https://szv-open.codehub.huawei.com/innersource/fenghuang/horizon/horizon-ecosystem.git"
},
"devDependencies": {
"@babel/core": "7.21.3",
"@babel/plugin-proposal-class-properties": "7.16.7",
"@babel/plugin-proposal-nullish-coalescing-operator": "7.16.7",
"@babel/plugin-proposal-object-rest-spread": "7.16.7",
"@babel/plugin-proposal-optional-chaining": "7.16.7",
"@babel/plugin-syntax-jsx": "7.16.7",
"@babel/plugin-transform-arrow-functions": "7.16.7",
"@babel/plugin-transform-block-scoped-functions": "7.16.7",
"@babel/plugin-transform-block-scoping": "7.16.7",
"@babel/plugin-transform-classes": "7.16.7",
"@babel/plugin-transform-computed-properties": "7.16.7",
"@babel/plugin-transform-destructuring": "7.16.7",
"@babel/plugin-transform-for-of": "7.16.7",
"@babel/plugin-transform-literals": "7.16.7",
"@babel/plugin-transform-object-assign": "7.16.7",
"@babel/plugin-transform-object-super": "7.16.7",
"@babel/plugin-transform-parameters": "7.16.7",
"@babel/plugin-transform-react-jsx": "7.16.7",
"@babel/plugin-transform-react-jsx-source": "^7.16.7",
"@babel/plugin-transform-runtime": "7.16.7",
"@babel/plugin-transform-shorthand-properties": "7.16.7",
"@babel/plugin-transform-spread": "7.16.7",
"@babel/plugin-transform-template-literals": "7.16.7",
"@babel/preset-env": "7.16.7",
"@babel/preset-typescript": "^7.16.7",
"@cloudsop/horizon": "0.0.52",
"@rollup/plugin-babel": "^6.0.3",
"@rollup/plugin-node-resolve": "^15.1.0",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^14.0.0",
"@testing-library/user-event": "^14.4.3",
"@types/jest": "^29.2.3",
"@types/react": "18.0.25",
"@types/react-redux": "7.0.0",
"jest": "29.3.1",
"jest-environment-jsdom": "^29.5.0",
"jsdom": "^21.1.1",
"prettier": "2.8.8",
"react-redux": "^3.8.1 || ^4.0.0",
"rollup": "2.79.1",
"rollup-plugin-execute": "^1.1.1",
"ts-jest": "29.0.3",
"typescript": "4.9.3"
},
"dependencies": {
"@cloudsop/horizon": "^0.0.52",
"react": "18.2.0",
"react-dom": "18.2.0"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"engines": {
"node": ">=12.0.0"
},
"engineStrict": true
}

View File

@ -0,0 +1,25 @@
import { Action, Path } from '../history/types';
type Location = Partial<Path>;
export declare enum ActionName {
LOCATION_CHANGE = "$horizon-router/LOCATION_CHANGE",
CALL_HISTORY_METHOD = "$horizon-router/CALL_HISTORY_METHOD"
}
export type ActionMessage = {
type: ActionName.LOCATION_CHANGE;
payload: {
location: Location;
action: Action;
isFirstRendering: boolean;
};
} | {
type: ActionName.CALL_HISTORY_METHOD;
payload: {
method: string;
args: any;
};
};
export declare const onLocationChanged: (location: Location, action: Action, isFirstRendering?: boolean) => ActionMessage;
export declare const push: (...args: any) => ActionMessage;
export declare const replace: (...args: any) => ActionMessage;
export declare const go: (...args: any) => ActionMessage;
export {};

View File

@ -0,0 +1,3 @@
import { ActionMessage } from './actions';
import { History } from '../history/types';
export declare function routerMiddleware(history: History): (_: any) => (next: any) => (action: ActionMessage) => any;

View File

@ -0,0 +1,11 @@
export { getConnectedRouter } from './connectedRouter';
export declare const connectRouter: (history: import("../router").History<unknown>) => (state?: {
location: Partial<import("../router").Location<unknown>> & {
query?: Record<string, any>;
};
action: import("../history/types").Action;
}, { type, payload }?: {
type?: import("./actions").ActionName;
payload?: any;
}) => any;
export { routerMiddleware } from './dispatch';

View File

@ -0,0 +1,16 @@
import { ActionName } from './actions';
import { Action, History } from '../history/types';
import { Location } from '../router';
type LocationWithQuery = Partial<Location> & {
query?: Record<string, any>;
};
type InitRouterState = {
location: LocationWithQuery;
action: Action;
};
type Payload = {
type?: ActionName;
payload?: any;
};
export declare function createConnectRouter(): (history: History) => (state?: InitRouterState, { type, payload }?: Payload) => any;
export {};

View File

@ -0,0 +1,10 @@
import { HistoryProps, Listener, Navigation, Prompt } from './types';
import transitionManager from './transitionManager';
export declare function getBaseHistory<S>(transitionManager: transitionManager<S>, setListener: (delta: number) => void, browserHistory: History): {
go: (step: number) => void;
goBack: () => void;
goForward: () => void;
listen: (listener: Listener<S>) => () => void;
block: (prompt?: Prompt<S>) => () => void;
getUpdateStateFunc: (historyProps: HistoryProps<S>) => (nextState: Navigation<S> | undefined) => void;
};

View File

@ -0,0 +1,8 @@
import { BaseOption, DefaultStateType, History } from './types';
export type BrowserHistoryOption = {
/**
* forceRefresh为True时跳转时会强制刷新页面
*/
forceRefresh?: boolean;
} & BaseOption;
export declare function createBrowserHistory<S = DefaultStateType>(options?: BrowserHistoryOption): History<S>;

View File

@ -0,0 +1,4 @@
export declare function isBrowser(): boolean;
export declare function getDefaultConfirmation(message: string, callBack: (result: boolean) => void): void;
export declare function isSupportHistory(): boolean;
export declare function isSupportsPopState(): boolean;

View File

@ -0,0 +1,7 @@
import { BaseOption, DefaultStateType, History } from './types';
export type urlHashType = 'slash' | 'noslash';
type HashHistoryOption = {
hashType?: urlHashType;
} & BaseOption;
export declare function createHashHistory<S = DefaultStateType>(option?: HashHistoryOption): History<S>;
export {};

View File

@ -0,0 +1,11 @@
import { Action, CallBackFunc, ConfirmationFunc, Listener, Location, Navigation, Prompt, TManager } from './types';
declare class TransitionManager<S> implements TManager<S> {
private prompt;
private listeners;
constructor();
setPrompt(prompt: Prompt<S>): () => void;
addListener(func: Listener<S>): () => void;
notifyListeners(args: Navigation<S>): void;
confirmJumpTo(location: Location<S>, action: Action, userConfirmationFunc: ConfirmationFunc, callBack: CallBackFunc): void;
}
export default TransitionManager;

View File

@ -0,0 +1,56 @@
export type BaseOption = {
basename?: string;
getUserConfirmation?: ConfirmationFunc;
};
export interface HistoryProps<T = unknown> {
readonly action: Action;
readonly location: Location<T>;
length: number;
}
export interface History<T = unknown> extends HistoryProps<T> {
createHref(path: Partial<Path>): string;
push(to: To, state?: T): void;
replace(to: To, state?: T): void;
listen(listener: Listener<T>): () => void;
block(prompt: Prompt<T>): () => void;
go(index: number): void;
goBack(): void;
goForward(): void;
}
export declare enum Action {
pop = "POP",
push = "PUSH",
replace = "REPLACE"
}
export declare enum EventType {
PopState = "popstate",
HashChange = "hashchange"
}
export type Path = {
pathname: string;
search: string;
hash: string;
};
export type HistoryState<T> = {
state?: T;
key: string;
};
export type DefaultStateType = unknown;
export type Location<T = unknown> = Path & HistoryState<T>;
export type To = string | Partial<Path>;
export interface Listener<T = unknown> {
(navigation: Navigation<T>): void;
}
export interface Navigation<T = unknown> {
action: Action;
location: Location<T>;
}
export type Prompt<S> = string | boolean | null | ((location: Location<S>, action: Action) => void);
export type CallBackFunc = (isJump: boolean) => void;
export type ConfirmationFunc = (message: string, callBack: CallBackFunc) => void;
export interface TManager<S> {
setPrompt(next: Prompt<S>): () => void;
addListener(func: (navigation: Navigation<S>) => void): () => void;
notifyListeners(args: Navigation<S>): void;
confirmJumpTo(location: Location<S>, action: Action, userConfirmationFunc: ConfirmationFunc, callBack: CallBackFunc): void;
}

View File

@ -0,0 +1,14 @@
import { Action, Location, Path, To } from './types';
export declare function createPath(path: Partial<Path>): string;
export declare function parsePath(url: string): Partial<Path>;
export declare function createLocation<S>(current: string | Location, to: To, state?: S, key?: string): Readonly<Location<S>>;
export declare function isLocationEqual(p1: Partial<Path>, p2: Partial<Path>): boolean;
export declare function addHeadSlash(path: string): string;
export declare function stripHeadSlash(path: string): string;
export declare function normalizeSlash(path: string): string;
export declare function hasBasename(path: string, prefix: string): Boolean;
export declare function stripBasename(path: string, prefix: string): string;
export declare function createMemoryRecord<T, S>(initVal: S, fn: (arg: S) => T): {
getDelta: (to: S, form: S) => number;
addRecord: (current: S, newRecord: S, action: Action) => void;
};

View File

@ -0,0 +1,2 @@
declare function warning(condition: any, message: string): void;
export default warning;

View File

@ -0,0 +1,12 @@
import { ReactNode } from 'react';
import { ConfirmationFunc } from '../history/types';
export type BaseRouterProps = {
basename: string;
getUserConfirmation: ConfirmationFunc;
children?: ReactNode;
};
export type BrowserRouterProps = BaseRouterProps & {
forceRefresh: boolean;
};
declare function BrowserRouter<P extends Partial<BrowserRouterProps>>(props: P): JSX.Element;
export default BrowserRouter;

View File

@ -0,0 +1,7 @@
import { BaseRouterProps } from './BrowserRouter';
import { urlHashType } from '../history/hashHistory';
export type HashRouterProps = BaseRouterProps & {
hashType: urlHashType;
};
declare function HashRouter<P extends Partial<HashRouterProps>>(props: P): JSX.Element;
export default HashRouter;

View File

@ -0,0 +1,18 @@
import * as React from 'react';
import { Location } from './index';
export type LinkProps = {
component?: React.ComponentType<any>;
to: Partial<Location> | string | ((location: Location) => string | Partial<Location>);
replace?: boolean;
tag?: string;
/**
* @deprecated
* React16以后不再需要该属性
**/
innerRef?: React.Ref<HTMLAnchorElement>;
} & React.AnchorHTMLAttributes<HTMLAnchorElement>;
declare function Link<P extends LinkProps>(props: P): React.DOMElement<{
href: string;
onClick: (event: React.MouseEvent<HTMLAnchorElement>) => void;
} & Omit<P, "replace" | "to" | "component" | "onClick" | "target">, Element>;
export default Link;

View File

@ -0,0 +1,10 @@
import type { LinkProps } from './Link';
import { Location } from './index';
import { Matched } from './matcher/parser';
type NavLinkProps = {
to: Partial<Location> | string | ((location: Location) => string | Partial<Location>);
isActive?: (match: Matched | null, location: Location) => boolean;
[key: string]: any;
} & LinkProps;
declare function NavLink<P extends NavLinkProps>(props: P): JSX.Element;
export default NavLink;

View File

@ -0,0 +1,8 @@
import { Location } from './index';
import { Action } from '../history/types';
type PromptProps = {
message?: string | ((location: Partial<Location>, action: Action) => void);
when?: boolean | ((location: Partial<Location>) => boolean);
};
declare function Prompt<P extends PromptProps>(props: P): JSX.Element;
export default Prompt;

View File

@ -0,0 +1,13 @@
import { Matched } from './matcher/parser';
import { Location } from './index';
export type RedirectProps = {
to: string | Partial<Location>;
push?: boolean;
path?: string;
from?: string;
exact?: boolean;
strict?: boolean;
readonly computed?: Matched | null;
};
declare function Redirect<P extends RedirectProps>(props: P): JSX.Element;
export default Redirect;

View File

@ -0,0 +1,23 @@
import * as React from 'react';
import { History, Location } from './index';
import { Matched } from './matcher/parser';
import { GetURLParams } from './matcher/types';
export type RouteComponentProps<P extends Record<string, any> = {}, S = unknown> = RouteChildrenProps<P, S>;
export type RouteChildrenProps<P extends Record<string, any> = {}, S = unknown> = {
history: History<S>;
location: Location<S>;
match: Matched<P> | null;
};
export type RouteProps<P extends Record<string, any> = {}, Path extends string = string> = {
location?: Location;
component?: React.ComponentType<RouteComponentProps<P>> | React.ComponentType<any> | undefined;
children?: ((props: RouteChildrenProps<P>) => React.ReactNode) | React.ReactNode;
render?: (props: RouteComponentProps<P>) => React.ReactNode;
path?: Path | Path[];
exact?: boolean;
sensitive?: boolean;
strict?: boolean;
computed?: Matched<P>;
};
declare function Route<Path extends string, P extends Record<string, any> = GetURLParams<Path>>(props: RouteProps<P, Path>): JSX.Element;
export default Route;

View File

@ -0,0 +1,8 @@
import * as React from 'react';
import { History } from '../history/types';
export type RouterProps = {
history: History;
children?: React.ReactNode;
};
declare function Router<P extends RouterProps>(props: P): JSX.Element;
export default Router;

View File

@ -0,0 +1,8 @@
import * as React from 'react';
import { Location } from './index';
export type SwitchProps = {
location?: Location;
children?: React.ReactNode;
};
declare function Switch<P extends SwitchProps>(props: P): React.ReactElement | null;
export default Switch;

View File

@ -0,0 +1 @@
import '@testing-library/jest-dom';

View File

@ -0,0 +1,8 @@
import { History, Location } from '../index';
export declare let historyHook: History;
export declare let locationHook: Location;
export declare const LocationDisplay: () => JSX.Element;
export declare const Test_Demo: () => JSX.Element;
export declare const Test_Demo2: () => JSX.Element;
export declare const Test_Demo3: () => JSX.Element;
export declare const Test_Demo4: () => JSX.Element;

View File

@ -0,0 +1,10 @@
/// <reference types="react" />
import { History, Location } from './index';
import { Matched } from './matcher/parser';
export type RouterContextValue = {
history: History;
location: Location;
match: Matched | null;
};
declare const RouterContext: import("react").Context<RouterContextValue>;
export default RouterContext;

View File

@ -0,0 +1,8 @@
import { Matched, Params } from './matcher/parser';
import { History } from '../history/types';
import { Location } from './index';
declare function useHistory<S>(): History<S>;
declare function useLocation<S>(): Location<S>;
declare function useParams<P>(): Params<P> | {};
declare function useRouteMatch<P>(path?: string): Matched<P> | null;
export { useHistory, useLocation, useParams, useRouteMatch };

View File

@ -0,0 +1,20 @@
import { Location as HLocation } from '../history/types';
type Location<S = unknown> = Omit<HLocation<S>, 'key'>;
export { Location };
export type { History } from '../history/types';
export { createBrowserHistory } from '../history/browerHistory';
export { createHashHistory } from '../history/hashHistory';
export { default as __RouterContext } from './context';
export { matchPath, generatePath } from './matcher/parser';
export { useHistory, useLocation, useParams, useRouteMatch } from './hooks';
export { default as Route } from './Route';
export { default as Router } from './Router';
export { default as Switch } from './Switch';
export { default as Redirect } from './Redirect';
export { default as Prompt } from './Prompt';
export { default as withRouter } from './withRouter';
export { default as HashRouter } from './HashRouter';
export { default as BrowserRouter } from './BrowserRouter';
export { default as Link } from './Link';
export { default as NavLink } from './NavLink';
export type { RouteComponentProps, RouteChildrenProps, RouteProps } from './Route';

View File

@ -0,0 +1,23 @@
import { Location as HLocation } from '../history/types';
type Location<S = unknown> = Omit<HLocation<S>, 'key'>;
export { Location };
export type { History } from '../history/types';
export { createBrowserHistory } from '../history/browerHistory';
export { createHashHistory } from '../history/hashHistory';
export { default as __RouterContext } from './context';
export { matchPath, generatePath } from './matcher/parser';
export { useHistory, useLocation, useParams, useRouteMatch } from './hooks';
export { default as Route } from './Route';
export { default as Router } from './Router';
export { default as Switch } from './Switch';
export { default as Redirect } from './Redirect';
export { default as Prompt } from './Prompt';
export { default as withRouter } from './withRouter';
export { default as HashRouter } from './HashRouter';
export { default as BrowserRouter } from './BrowserRouter';
export { default as Link } from './Link';
export { default as NavLink } from './NavLink';
export type { RouteComponentProps, RouteChildrenProps, RouteProps } from './Route';
export { connectRouter, routerMiddleware } from '../connect-router';
export declare const ConnectedRouter: any;
export declare const ConnectedHRouter: any;

View File

@ -0,0 +1,7 @@
export type LifeCycleProps = {
onMount?: () => void;
onUpdate?: (prevProps?: LifeCycleProps) => void;
onUnmount?: () => void;
data?: any;
};
export declare function LifeCycle(props: LifeCycleProps): any;

View File

@ -0,0 +1 @@
export {};

View File

@ -0,0 +1 @@
export {};

View File

@ -0,0 +1 @@
export {};

View File

@ -0,0 +1,2 @@
import { Token } from './types';
export declare function lexer(path: string): Token[];

View File

@ -0,0 +1,18 @@
import { GetURLParams, Parser, ParserOption } from './types';
export type Params<P> = {
[K in keyof P]?: P[K];
};
export type Matched<P = any> = {
score: number[];
params: Params<P>;
path: string;
url: string;
isExact: boolean;
};
export declare function createPathParser<Str extends string>(pathname: Str, option?: ParserOption): Parser<GetURLParams<Str>>;
export declare function createPathParser<P = unknown>(pathname: string, option?: ParserOption): Parser<P>;
/**
* @description 使pathname与pattern进行匹配
*/
export declare function matchPath<P = any>(pathname: string, pattern: string | string[], option?: ParserOption): Matched<P> | null;
export declare function generatePath<P = any>(path: string, params: Params<P>): string;

View File

@ -0,0 +1,36 @@
import { Matched, Params } from './parser';
export type Token = {
type: TokenType;
value: string;
};
export declare enum TokenType {
Delimiter = "delimiter",
Static = "static",
Param = "param",
WildCard = "wildcard",
LBracket = "(",
RBracket = ")",
Pattern = "pattern"
}
export interface Parser<P> {
regexp: RegExp;
keys: string[];
parse(url: string): Matched<P> | null;
compile(params: Params<P>): string;
}
export type ParserOption = {
caseSensitive?: boolean;
strictMode?: boolean;
exact?: boolean;
};
type ClearLeading<U extends string> = U extends `/${infer R}` ? ClearLeading<R> : U;
type ClearTailing<U extends string> = U extends `${infer L}/` ? ClearTailing<L> : U;
type ParseParam<Param extends string> = Param extends `:${infer R}` ? {
[K in R]: string;
} : {};
type MergeParams<OneParam extends Record<string, any>, OtherParam extends Record<string, any>> = {
readonly [Key in keyof OneParam | keyof OtherParam]?: string;
};
type ParseURLString<Str extends string> = Str extends `${infer Param}/${infer Rest}` ? MergeParams<ParseParam<Param>, ParseURLString<ClearLeading<Rest>>> : ParseParam<Str>;
export type GetURLParams<U extends string> = ParseURLString<ClearLeading<ClearTailing<U>>>;
export {};

View File

@ -0,0 +1,6 @@
/**
* @description url中的//转换为/
*/
export declare function cleanPath(path: string): string;
export declare function scoreCompare(score1: number[], score2: number[]): number;
export declare function escapeStr(str: string): string;

View File

@ -0,0 +1,3 @@
import * as React from 'react';
declare function withRouter<C extends React.ComponentType>(Component: C): (props: any) => JSX.Element;
export default withRouter;

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,9 @@
{
"module": "./esm/connectRouter.js",
"main": "./cjs/connectRouter.js",
"types": "./@types/router/index2.d.ts",
"peerDependencies": {
"react-redux": "^6.0.0 || ^7.1.0",
"redux": "^3.6.0 || ^4.0.0"
}
}

View File

@ -0,0 +1,51 @@
import { Action, Path } from '../history/types';
type Location = Partial<Path>
// 定义位置变化和history方法调用的Action type
export enum ActionName {
LOCATION_CHANGE = '$horizon-router/LOCATION_CHANGE',
CALL_HISTORY_METHOD = '$horizon-router/CALL_HISTORY_METHOD'
}
// 定义Action的两种数据类型
export type ActionMessage = {
type: ActionName.LOCATION_CHANGE
payload: {
location: Location,
action: Action
isFirstRendering: boolean
}
} | {
type: ActionName.CALL_HISTORY_METHOD
payload: {
method: string,
args: any
}
}
export const onLocationChanged = (location: Location, action: Action, isFirstRendering = false): ActionMessage => {
return {
type: ActionName.LOCATION_CHANGE,
payload: {
location,
action,
isFirstRendering,
},
};
};
const updateLocation = (method: string): (...args: any) => ActionMessage => {
return (...args: any) => ({
type: ActionName.CALL_HISTORY_METHOD,
payload: {
method,
args,
},
});
};
export const push = updateLocation('push');
export const replace = updateLocation('replace');
export const go = updateLocation('go');

View File

@ -0,0 +1,128 @@
import * as React from 'react';
import { useLayoutEffect } from 'react';
import { connect, ReactReduxContext } from 'react-redux';
import { Store } from 'redux';
import { reduxAdapter } from '@cloudsop/horizon';
import { History, Location, Router } from '../router';
import { Action, DefaultStateType, Navigation } from '../history/types';
import { ActionMessage, onLocationChanged } from './actions';
import stateReader from './reduxUtils';
type StoreType = 'HorizonXCompat' | 'Redux';
type ConnectedRouter<S> = {
store: Store;
history: History<S>;
basename: string;
children?: (() => React.ReactNode) | React.ReactNode;
onLocationChanged: (location: Location<S>, action: Action, isFirstRendering: boolean) => ActionMessage;
noInitialPop: boolean;
omitRouter: boolean;
storeType: StoreType;
};
const { connect: hConnect } = reduxAdapter;
function ConnectedRouterWithoutMemo<S>(props: ConnectedRouter<S>) {
const { store, history, onLocationChanged, omitRouter, children, storeType } = props;
const { getLocation } = stateReader(storeType);
// 监听store变化
const unsubscribe = store.subscribe(() => {
// 获取redux State中的location信息
const {
pathname: pathnameInStore,
search: searchInStore,
hash: hashInStore,
state: stateInStore,
} = getLocation<S>(store.getState());
// 获取当前history对象中的location信息
const {
pathname: pathnameInHistory,
search: searchInHistory,
hash: hashInHistory,
state: stateInHistory,
} = history.location;
// 两个location不一致 执行跳转
if (
history.action === 'PUSH' &&
(pathnameInHistory !== pathnameInStore ||
searchInHistory !== searchInStore ||
hashInHistory !== hashInStore ||
stateInHistory !== stateInStore)
) {
history.push(
{
pathname: pathnameInStore,
search: searchInStore,
hash: hashInStore,
},
stateInStore,
);
}
});
const handleLocationChange = (args: Navigation<S>, isFirstRendering: boolean = false) => {
const { location, action } = args;
onLocationChanged(location, action, isFirstRendering);
};
// 监听history更新
const unListen = () => history.listen(handleLocationChange);
useLayoutEffect(() => {
return () => {
unListen();
unsubscribe();
};
}, []);
if (!props.noInitialPop) {
// 传递初始时位置信息isFirstRendering设为true防止重复渲染
handleLocationChange({ location: history.location, action: history.action }, true);
}
if (omitRouter) {
return <>{children}</>;
}
let childrenNode: React.ReactNode;
if (typeof children === 'function') {
childrenNode = children();
} else {
childrenNode = children;
}
return <Router history={history}>{childrenNode}</Router>;
}
function getConnectedRouter<S = DefaultStateType>(type: StoreType) {
const mapDispatchToProps = (dispatch: any) => ({
onLocationChanged: (location: Location, action: Action, isFirstRendering: boolean) =>
dispatch(onLocationChanged(location, action, isFirstRendering)),
});
const ConnectedRouter = React.memo(ConnectedRouterWithoutMemo<S>);
const ConnectedRouterWithContext = (props: any) => {
const Context = props.context || ReactReduxContext;
return (
<Context.Consumer>
{({ store }: any) => <ConnectedRouter store={store} storeType={type} {...props} />}
</Context.Consumer>
);
};
// 针对不同的Store类型使用对应的connect函数
if (type === 'HorizonXCompat') {
return hConnect(null, mapDispatchToProps)(ConnectedRouterWithContext);
}
if (type === 'Redux') {
return connect(null, mapDispatchToProps)(ConnectedRouterWithContext);
} else {
throw new Error('Invalid store type');
}
}
export { getConnectedRouter };

View File

@ -0,0 +1,19 @@
import { ActionMessage, ActionName } from './actions';
import { History } from '../history/types';
// 定义connect-router对应的redux dispatch函数
export function routerMiddleware(history: History) {
return function(_: any) {
return function(next: any) {
return function(action: ActionMessage) {
if (action.type !== ActionName.CALL_HISTORY_METHOD) {
return next(action);
}
const { payload: { method, args } } = action;
if (method in history) {
(history as any)[method](...args);
}
};
};
};
}

View File

@ -0,0 +1,5 @@
import { createConnectRouter } from './reducer';
export { getConnectedRouter } from './connectedRouter';
export const connectRouter = createConnectRouter();
export { routerMiddleware } from './dispatch';

View File

@ -0,0 +1,62 @@
import { ActionName } from './actions';
import { Action, History } from '../history/types';
import { Location } from '../router';
type LocationWithQuery = Partial<Location> & { query?: Record<string, any> };
// 解析location对象将其中的query参数解析并注入
function injectQueryParams(location?: LocationWithQuery): LocationWithQuery {
if (location && location.query) {
return location;
}
const queryString = location && location.search;
if (!queryString) {
return {
...location,
query: {},
};
}
const queryObject: Record<string, any> = {};
const params = new URLSearchParams(queryString);
params.forEach((value, key) => (queryObject[key] = value));
return {
...location,
query: queryObject,
};
}
type InitRouterState = {
location: LocationWithQuery;
action: Action;
};
type Payload = {
type?: ActionName;
payload?: any;
};
export function createConnectRouter() {
// 初始化redux State
return (history: History) => {
const initRouterState = {
location: injectQueryParams(history.location),
action: history.action,
};
// 定义connect-router对应的redux reducer函数
return (state: InitRouterState = initRouterState, { type, payload }: Payload = {}): any => {
if (type === ActionName.LOCATION_CHANGE) {
const { location, action, isFirstRendering } = payload;
if (isFirstRendering) {
return state;
}
return { ...state, location: injectQueryParams(location), action: action };
}
return state;
};
};
}

View File

@ -0,0 +1,48 @@
import { Location } from '../router';
import { Action } from '../history/types';
// 获取redux state中的值
export function getIn(state: Record<string, any>, path: string[]): any {
if (!state) {
return state;
}
const length = path.length;
if (!length) {
return undefined;
}
let res = state;
for (let i = 0; i < length && !!res; ++i) {
res = res[path[i]];
}
return res;
}
// 从store的state中获取Router、Location、Action、Hash等信息
const stateReader = (storeType: string) => {
const isRouter = (value: unknown) => {
return value !== null && typeof value === 'object' && !!getIn(value, ['location']) && !!getIn(value, ['action']);
};
const getRouter = (state: any) => {
const router = getIn(state, ['router']);
if (!isRouter(router)) {
throw new Error(`Could not find router reducer in ${storeType} store, it must be mounted under "router"`);
}
return router!;
};
const getLocation = <S>(state: any): Partial<Location<S>> => getIn(getRouter(state), ['location']);
const getAction = (state: any): Action => getIn(getRouter(state), ['action']);
const getSearch = (state: any): string => getIn(getRouter(state), ['location', 'search']);
const getHash = (state: any): string => getIn(getRouter(state), ['location', 'hash']);
return {
getHash,
getAction,
getSearch,
getRouter,
getLocation,
};
};
export default stateReader

View File

@ -0,0 +1,61 @@
import { createPath } from '../utils';
describe('createPath', () => {
describe('given only a pathname', () => {
it('returns the pathname unchanged', () => {
let path = createPath({ pathname: 'https://google.com' });
expect(path).toBe('https://google.com');
});
});
describe('given a pathname and a search param', () => {
it('returns the constructed pathname', () => {
let path = createPath({
pathname: 'https://google.com',
search: '?something=cool',
});
expect(path).toBe('https://google.com?something=cool');
});
});
describe('given a pathname and a search param without ?', () => {
it('returns the constructed pathname', () => {
let path = createPath({
pathname: 'https://google.com',
search: 'something=cool',
});
expect(path).toBe('https://google.com?something=cool');
});
});
describe('given a pathname and a hash param', () => {
it('returns the constructed pathname', () => {
let path = createPath({
pathname: 'https://google.com',
hash: '#section-1',
});
expect(path).toBe('https://google.com#section-1');
});
});
describe('given a pathname and a hash param without #', () => {
it('returns the constructed pathname', () => {
let path = createPath({
pathname: 'https://google.com',
hash: 'section-1',
});
expect(path).toBe('https://google.com#section-1');
});
});
describe('given a full location object', () => {
it('returns the constructed pathname', () => {
let path = createPath({
pathname: 'https://google.com',
search: 'something=cool',
hash: '#section-1',
});
expect(path).toBe('https://google.com?something=cool#section-1');
});
});
});

View File

@ -0,0 +1,60 @@
import { HistoryProps, Listener, Navigation, Prompt } from './types';
import transitionManager from './transitionManager';
// 抽取BrowserHistory和HashHistory中相同的方法
export function getBaseHistory<S>(
transitionManager: transitionManager<S>,
setListener: (delta: number) => void,
browserHistory: History,
) {
function go(step: number) {
browserHistory.go(step);
}
function goBack() {
browserHistory.go(-1);
}
function goForward() {
browserHistory.go(1);
}
function listen(listener: Listener<S>): () => void {
const cancel = transitionManager.addListener(listener);
setListener(1);
return () => {
setListener(-1);
cancel();
};
}
let isBlocked = false;
function block(prompt: Prompt<S> = false): () => void {
const unblock = transitionManager.setPrompt(prompt);
if (!isBlocked) {
setListener(1);
isBlocked = true;
}
return () => {
if (isBlocked) {
isBlocked = false;
setListener(-1);
}
unblock();
};
}
function getUpdateStateFunc(historyProps: HistoryProps<S>) {
return function (nextState: Navigation<S> | undefined) {
if (nextState) {
Object.assign(historyProps, nextState);
}
historyProps.length = browserHistory.length;
const args = { location: historyProps.location, action: historyProps.action };
transitionManager.notifyListeners(args);
};
}
return { go, goBack, goForward, listen, block, getUpdateStateFunc };
}

View File

@ -0,0 +1,179 @@
import { getDefaultConfirmation, isSupportHistory, isSupportsPopState } from './dom';
import { Action, BaseOption, DefaultStateType, EventType, History, HistoryState, Location, Path, To } from './types';
import { normalizeSlash, createMemoryRecord, createPath, createLocation, stripBasename } from './utils';
import TransitionManager from './transitionManager';
import warning from './waring';
import { getBaseHistory } from './baseHistory';
export type BrowserHistoryOption = {
/**
* forceRefresh为True时跳转时会强制刷新页面
*/
forceRefresh?: boolean;
} & BaseOption;
export function createBrowserHistory<S = DefaultStateType>(options: BrowserHistoryOption = {}): History<S> {
const supportHistory = isSupportHistory();
const isSupportPopState = isSupportsPopState();
const browserHistory = window.history;
const { forceRefresh = false, getUserConfirmation = getDefaultConfirmation } = options;
const basename = options.basename ? normalizeSlash(options.basename) : '';
const initLocation = getLocation(getHistoryState());
const recordOperator = createMemoryRecord<string, Location<S>>(initLocation, l => l.key);
const transitionManager = new TransitionManager<S>();
const { go, goBack, goForward, listen, block, getUpdateStateFunc } = getBaseHistory<S>(
transitionManager,
setListener,
browserHistory,
);
const history: History<S> = {
action: Action.pop,
length: browserHistory.length,
location: initLocation,
go,
goBack,
goForward,
listen,
block,
push,
replace,
createHref,
};
const updateState = getUpdateStateFunc(history);
function getHistoryState() {
return supportHistory ? window.history.state : {};
}
function getLocation(historyState: Partial<HistoryState<S>>) {
const { search, hash } = window.location;
const { key, state } = historyState || {};
let pathname = window.location.pathname;
pathname = basename ? stripBasename(pathname, basename) : pathname;
return createLocation<S>('', { pathname, search, hash }, state, key);
}
// 拦截页面POP事件后防止返回到的页面被重复拦截
let forceJump = false;
function handlePopState(location: Location<S>) {
if (forceJump) {
forceJump = false;
updateState(undefined);
} else {
const action = Action.pop;
const callback = (isJump: boolean) => {
if (isJump) {
// 执行跳转行为
updateState({ action: action, location: location });
} else {
revertPopState(location, history.location);
}
};
transitionManager.confirmJumpTo(location, action, getUserConfirmation, callback);
}
}
function popStateListener(event: PopStateEvent) {
handlePopState(getLocation(event.state));
}
function hashChangeListener() {
const location = getLocation(getHistoryState());
handlePopState(location);
}
let listenerCount = 0;
function setListener(count: number) {
listenerCount += count;
if (listenerCount === 1 && count === 1) {
window.addEventListener(EventType.PopState, popStateListener);
if (!isSupportPopState) {
window.addEventListener(EventType.HashChange, hashChangeListener);
}
} else if (listenerCount === 0) {
window.removeEventListener(EventType.PopState, popStateListener);
if (!isSupportPopState) {
window.removeEventListener(EventType.HashChange, hashChangeListener);
}
}
}
// 取消页面跳转并恢复到跳转前的页面
function revertPopState(form: Location<S>, to: Location<S>) {
const delta = recordOperator.getDelta(to, form);
if (delta !== 0) {
go(delta);
forceJump = true;
}
}
function createHref(path: Partial<Path>) {
return basename + createPath(path);
}
function push(to: To, state?: S) {
const action = Action.push;
const location = createLocation<S>(history.location, to, state, undefined);
transitionManager.confirmJumpTo(location, action, getUserConfirmation, isJump => {
if (!isJump) {
return;
}
const href = createHref(location);
const { key, state } = location;
if (supportHistory) {
if (forceRefresh) {
window.location.href = href;
} else {
browserHistory.pushState({ key: key, state: state }, '', href);
recordOperator.addRecord(history.location, location, action);
updateState({ action, location });
}
} else {
warning(state !== undefined, 'Browser history cannot push state in browsers that do not support HTML5 history');
window.location.href = href;
}
});
}
function replace(to: To, state?: S) {
const action = Action.replace;
const location = createLocation<S>(history.location, to, state, undefined);
transitionManager.confirmJumpTo(location, action, getUserConfirmation, isJump => {
if (!isJump) {
return;
}
const href = createHref(location);
const { key, state } = location;
if (supportHistory) {
if (forceRefresh) {
window.location.replace(href);
} else {
browserHistory.replaceState({ key: key, state: state }, '', href);
recordOperator.addRecord(history.location, location, action);
updateState({ action, location });
}
} else {
warning(state !== undefined, 'Browser history cannot push state in browsers that do not support HTML5 history');
window.location.replace(href);
}
});
}
return history;
}

View File

@ -0,0 +1,17 @@
export function isBrowser(): boolean {
return typeof window !== 'undefined' && window.document && typeof window.document.createElement === 'function';
}
export function getDefaultConfirmation(message: string, callBack: (result: boolean) => void) {
callBack(window.confirm(message));
}
// 判断浏览器是否支持pushState方法pushState是browserHistory实现的基础
export function isSupportHistory(): boolean {
return isBrowser() && window.history && 'pushState' in window.history;
}
// 判断浏览器是否支持PopState事件
export function isSupportsPopState(): boolean {
return window.navigator.userAgent.indexOf('Trident') === -1;
}

View File

@ -0,0 +1,197 @@
import { Action, BaseOption, DefaultStateType, EventType, History, Location, To } from './types';
import {
addHeadSlash,
normalizeSlash,
createMemoryRecord,
createPath,
createLocation,
isLocationEqual,
stripBasename,
stripHeadSlash,
} from './utils';
import { getDefaultConfirmation } from './dom';
import TransitionManager from './transitionManager';
import warning from './waring';
import { getBaseHistory } from './baseHistory';
export type urlHashType = 'slash' | 'noslash';
type HashHistoryOption = {
hashType?: urlHashType;
} & BaseOption;
// 获取#前的内容
function stripHash(path: string): string {
const idx = path.indexOf('#');
return idx === -1 ? path : path.substring(0, idx);
}
// 获取#后的内容
function getHashContent(path: string): string {
const idx = path.indexOf('#');
return idx === -1 ? '' : path.substring(idx + 1);
}
export function createHashHistory<S = DefaultStateType>(option: HashHistoryOption = {}): History<S> {
const browserHistory = window.history;
const { hashType = 'slash', getUserConfirmation = getDefaultConfirmation } = option;
const basename = option.basename ? normalizeSlash(option.basename) : '';
const pathDecoder = addHeadSlash;
const pathEncoder = hashType === 'slash' ? addHeadSlash : stripHeadSlash;
function getLocation() {
let hashPath = pathDecoder(getHashContent(window.location.hash));
if (basename) {
hashPath = stripBasename(hashPath, basename);
}
return createLocation<S>('', hashPath, undefined, 'default');
}
const initLocation = getLocation();
const memRecords = createMemoryRecord<string, Location<S>>(initLocation, createPath);
const transitionManager = new TransitionManager<S>();
function createHref(location: Location<S>) {
const tag = document.querySelector('base');
const base = tag && tag.getAttribute('href') ? stripHash(window.location.href) : '';
return base + '#' + pathEncoder(basename + createPath(location));
}
let forceNextPop = false;
let ignorePath: null | string = null;
const { go, goBack, goForward, listen, block, getUpdateStateFunc } = getBaseHistory(
transitionManager,
setListener,
browserHistory,
);
const history: History<S> = {
action: Action.pop,
length: browserHistory.length,
location: initLocation,
go,
goBack,
goForward,
push,
replace,
listen,
block,
createHref,
};
const updateState = getUpdateStateFunc(history);
function push(to: To, state?: S) {
warning(state !== undefined, 'Hash history does not support state, it will be ignored');
const action = Action.push;
const location = createLocation<S>(history.location, to, undefined, '');
transitionManager.confirmJumpTo(location, action, getUserConfirmation, isJump => {
if (!isJump) {
return;
}
const path = createPath(location);
const encodedPath = pathEncoder(basename + path);
// 前后hash不一样才进行跳转
if (getHashContent(window.location.href) !== encodedPath) {
ignorePath = encodedPath;
window.location.hash = encodedPath;
memRecords.addRecord(history.location, location, action);
updateState({ action, location });
} else {
updateState(undefined);
}
});
}
function replace(to: To, state?: S) {
warning(state !== undefined, 'Hash history does not support state, it will be ignored');
const action = Action.replace;
const location = createLocation<S>(history.location, to, undefined, '');
transitionManager.confirmJumpTo(location, action, getUserConfirmation, isJump => {
if (!isJump) {
return;
}
const path = createPath(location);
const encodedPath = pathEncoder(basename + path);
if (getHashContent(window.location.href) !== encodedPath) {
ignorePath = path;
window.location.replace(stripHash(window.location.href) + '#' + encodedPath);
}
memRecords.addRecord(history.location, location, action);
updateState({ action, location });
});
}
function handleHashChange() {
const hashPath = getHashContent(window.location.href);
const encodedPath = pathEncoder(hashPath);
if (hashPath !== encodedPath) {
window.location.replace(stripHash(window.location.href) + '#' + encodedPath);
} else {
const location = getLocation();
const prevLocation = history.location;
if (!forceNextPop && isLocationEqual(location, prevLocation)) {
return;
}
if (ignorePath === createPath(location)) {
return;
}
ignorePath = null;
handlePopState(location);
}
}
function handlePopState(location: Location<S>) {
if (forceNextPop) {
forceNextPop = false;
updateState(undefined);
} else {
const action = Action.pop;
const callback = (isJump: boolean) => {
if (isJump) {
updateState({ action: action, location: location });
} else {
revertPopState(location);
}
};
transitionManager.confirmJumpTo(location, action, getUserConfirmation, callback);
}
}
// 在跳转行为被Block后用History.go()跳转回之前的页面
function revertPopState(form: Location<S>) {
const to = history.location;
const delta = memRecords.getDelta(to, form);
if (delta !== 0) {
go(delta);
forceNextPop = true;
}
}
let listenerCount = 0;
function setListener(delta: number) {
listenerCount += delta;
if (listenerCount === 1 && delta === 1) {
window.addEventListener(EventType.HashChange, handleHashChange);
} else if (listenerCount === 0) {
window.removeEventListener(EventType.HashChange, handleHashChange);
}
}
return history;
}

View File

@ -0,0 +1,64 @@
import { Action, CallBackFunc, ConfirmationFunc, Listener, Location, Navigation, Prompt, TManager } from './types';
class TransitionManager<S> implements TManager<S> {
private prompt: Prompt<S>;
private listeners: Listener<S>[];
constructor() {
this.prompt = null;
this.listeners = [];
}
public setPrompt(prompt: Prompt<S>): () => void {
this.prompt = prompt;
// 清除Prompt
return () => {
if (this.prompt === prompt) {
this.prompt = null;
}
};
}
// 使用发布订阅模式管理history的监听者
public addListener(func: Listener<S>): () => void {
let isActive = true;
const listener = (args: Navigation<S>) => {
if (isActive) {
func(args);
}
};
this.listeners.push(listener);
return () => {
isActive = false;
// 移除对应的监听者
this.listeners = this.listeners.filter(item => item !== listener);
};
}
public notifyListeners(args: Navigation<S>) {
for (const listener of this.listeners) {
listener(args);
}
}
public confirmJumpTo(
location: Location<S>,
action: Action,
userConfirmationFunc: ConfirmationFunc,
callBack: CallBackFunc
) {
if (this.prompt !== null) {
const result = typeof this.prompt === 'function' ? this.prompt(location, action) : this.prompt;
if (typeof result === 'string') {
typeof userConfirmationFunc === 'function' ? userConfirmationFunc(result, callBack) : callBack(true);
} else {
callBack(result !== false);
}
} else {
callBack(true);
}
}
}
export default TransitionManager;

View File

@ -0,0 +1,92 @@
export type BaseOption = {
basename?: string;
getUserConfirmation?: ConfirmationFunc;
};
export interface HistoryProps<T = unknown> {
readonly action: Action;
readonly location: Location<T>;
length: number;
}
export interface History<T = unknown> extends HistoryProps<T> {
createHref(path: Partial<Path>): string;
push(to: To, state?: T): void;
replace(to: To, state?: T): void;
listen(listener: Listener<T>): () => void;
block(prompt: Prompt<T>): () => void;
go(index: number): void;
goBack(): void;
goForward(): void;
}
export enum Action {
pop = 'POP',
push = 'PUSH',
replace = 'REPLACE',
}
export enum EventType {
PopState = 'popstate',
HashChange = 'hashchange',
}
export type Path = {
pathname: string;
search: string;
hash: string;
};
export type HistoryState<T> = {
state?: T;
key: string;
};
export type DefaultStateType = unknown;
export type Location<T = unknown> = Path & HistoryState<T>;
export type To = string | Partial<Path>;
export interface Listener<T = unknown> {
(navigation: Navigation<T>): void;
}
export interface Navigation<T = unknown> {
action: Action;
location: Location<T>;
}
export type Prompt<S> = string | boolean | null | ((location: Location<S>, action: Action) => void);
export type CallBackFunc = (isJump: boolean) => void;
export type ConfirmationFunc = (message: string, callBack: CallBackFunc) => void;
export interface TManager<S> {
setPrompt(next: Prompt<S>): () => void;
addListener(func: (navigation: Navigation<S>) => void): () => void;
notifyListeners(args: Navigation<S>): void;
confirmJumpTo(
location: Location<S>,
action: Action,
userConfirmationFunc: ConfirmationFunc,
callBack: CallBackFunc,
): void;
}

View File

@ -0,0 +1,134 @@
import { Action, Location, Path, To } from './types';
export function createPath(path: Partial<Path>): string {
const { search, hash } = path;
let pathname = path.pathname || '/';
if (search && search !== '?') {
pathname += search.startsWith('?') ? search : '?' + search;
}
if (hash && hash !== '#') {
pathname += hash.startsWith('#') ? hash : '#' + hash;
}
return pathname;
}
export function parsePath(url: string): Partial<Path> {
if (!url) {
return {};
}
let parsedPath: Partial<Path> = {};
let hashIdx = url.indexOf('#');
if (hashIdx > -1) {
parsedPath.hash = url.substring(hashIdx);
url = url.substring(0, hashIdx);
}
let searchIdx = url.indexOf('?');
if (searchIdx > -1) {
parsedPath.search = url.substring(searchIdx);
url = url.substring(0, searchIdx);
}
if (url) {
parsedPath.pathname = url;
}
return parsedPath;
}
export function createLocation<S>(current: string | Location, to: To, state?: S, key?: string): Readonly<Location<S>> {
let pathname = typeof current === 'string' ? current : current.pathname;
let urlObj = typeof to === 'string' ? parsePath(to) : to;
// 随机key长度取6
const getRandKey = genRandomKey(6);
const location = {
pathname: pathname,
search: '',
hash: '',
state: state,
key: typeof key === 'string' ? key : getRandKey(),
...urlObj,
};
if (!location.pathname) {
location.pathname = '/';
}
return location;
}
export function isLocationEqual(p1: Partial<Path>, p2: Partial<Path>) {
return p1.pathname === p2.pathname && p1.search === p2.search && p1.hash === p2.hash;
}
export function addHeadSlash(path: string): string {
if (path[0] === '/') {
return path;
}
return '/' + path;
}
export function stripHeadSlash(path: string): string {
if (path[0] === '/') {
return path.substring(1);
}
return path;
}
export function normalizeSlash(path: string): string {
const tempPath = addHeadSlash(path);
if (tempPath[tempPath.length - 1] === '/') {
return tempPath.substring(0, tempPath.length - 1);
}
return tempPath;
}
export function hasBasename(path: string, prefix: string): Boolean {
return (
path.toLowerCase().indexOf(prefix.toLowerCase()) === 0 && ['/', '?', '#', ''].includes(path.charAt(prefix.length))
);
}
export function stripBasename(path: string, prefix: string): string {
return hasBasename(path, prefix) ? path.substring(prefix.length) : path;
}
// 使用随机生成的Key记录被访问过的URL当Block被被触发时利用delta值跳转到之前的页面
export function createMemoryRecord<T, S>(initVal: S, fn: (arg: S) => T) {
let visitedRecord: T[] = [fn(initVal)];
function getDelta(to: S, form: S): number {
let toIdx = visitedRecord.lastIndexOf(fn(to));
if (toIdx === -1) {
toIdx = 0;
}
let fromIdx = visitedRecord.lastIndexOf(fn(form));
if (fromIdx === -1) {
fromIdx = 0;
}
return toIdx - fromIdx;
}
function addRecord(current: S, newRecord: S, action: Action) {
const curVal = fn(current);
const NewVal = fn(newRecord);
if (action === Action.push) {
const prevIdx = visitedRecord.lastIndexOf(curVal);
const newVisitedRecord = visitedRecord.slice(0, prevIdx + 1);
newVisitedRecord.push(NewVal);
visitedRecord = newVisitedRecord;
}
if (action === Action.replace) {
const prevIdx = visitedRecord.lastIndexOf(curVal);
if (prevIdx !== -1) {
visitedRecord[prevIdx] = NewVal;
}
}
}
return { getDelta, addRecord };
}
function genRandomKey(length: number): () => string {
const end = length + 2;
return () => {
return Math.random().toString(18).substring(2, end);
};
}

View File

@ -0,0 +1,9 @@
function warning(condition: any, message: string) {
if (condition) {
if (console && typeof console.warn === 'function') {
console.warn(message);
}
}
}
export default warning;

View File

@ -0,0 +1,32 @@
import * as React from 'react';
import { useRef, ReactNode } from 'react';
import Router from './Router';
import { createBrowserHistory } from '../history/browerHistory';
import { ConfirmationFunc, History } from '../history/types';
export type BaseRouterProps = {
basename: string;
getUserConfirmation: ConfirmationFunc;
children?: ReactNode;
};
export type BrowserRouterProps = BaseRouterProps & {
forceRefresh: boolean;
};
function BrowserRouter<P extends Partial<BrowserRouterProps>>(props: P) {
// 使用Ref持有History对象防止重复渲染
let historyRef = useRef<History>();
if (historyRef.current === null || historyRef.current === undefined) {
historyRef.current = createBrowserHistory({
basename: props.basename,
forceRefresh: props.forceRefresh,
getUserConfirmation: props.getUserConfirmation,
});
}
return <Router history={historyRef.current}>{props.children}</Router>;
}
export default BrowserRouter;

Some files were not shown because too many files have changed in this diff Show More