主文件:
import React, { useContext, useEffect } from 'react';
import { Route, RouteProps, Routes, useLocation, useMatch } from 'react-router-dom';
import AuthenticationContext from './AuthenticationContext';
export type PrivateRouteProps = RouteProps & {
Element?: React.ComponentType<any>;
};
/* eslint-disable react/jsx-props-no-spreading */
function PrivateRoute({ children, path, Element, ...routePropsWithoutChildrenAndComponent }: any) {
const { authorize, authenticated, authenticating, callbackPath } =
useContext(AuthenticationContext);
const location = useLocation();
const match = useMatch(path?.toString() || '*');
useEffect(() => {
if (!authenticated && !authenticating && match && location.pathname !== callbackPath) {
const authorizeAsync = async () => {
authorize(location as unknown as URL);
};
authorizeAsync();
}
}, [authorize, match, location, authenticated, authenticating, callbackPath]);
if (!authenticated) {
return null;
}
return (
<Routes>
<Route
{...routePropsWithoutChildrenAndComponent}
element={Element ? <Element /> : children}
/>
</Routes>
);
}
export default PrivateRoute;
测试文件:
/* eslint-disable react/prop-types */
import React from 'react';
import { render } from '@testing-library/react';
import { createMemoryHistory } from 'history';
import { unstable_HistoryRouter as Router } from 'react-router-dom';
import { PrivateRoute } from '..';
import AuthenticationContext, { AuthenticationContextProps } from '../AuthenticationContext';
describe('PrivateRoute', () => {
function renderWithRouterAndContext(
content: JSX.Element,
// eslint-disable-next-line @typescript-eslint/ban-types
{ location, context } = {} as { location: string; context: object },
) {
const history = createMemoryHistory({ initialEntries: [location] });
const defaultContext: AuthenticationContextProps = {
fetchToken: () => 'xyz' as any,
callbackPath: '/oauth',
setError: () => {},
authenticated: false,
authenticating: false,
authorize: () => {},
logout: () => {},
getToken: () => 'xyz',
};
const wrapper = ({ children }: { children: React.ReactNode }) => (
<AuthenticationContext.Provider value={{ ...defaultContext, ...context }}>
<Router history={history as any}>{children}</Router>
</AuthenticationContext.Provider>
);
return {
...render(content, { wrapper } as any),
history,
};
}
describe('when authenticated', () => {
it('can render children', () => {
const { container } = renderWithRouterAndContext(
<PrivateRoute path="/hello">
<h1>Hello</h1>
</PrivateRoute>,
{ location: '/hello', context: { callbackPath: '/oauth', authenticated: true } },
);
expect(container.innerHTML).toBe('<h1>Hello</h1>');
});
it('can render a component', () => {
function MyComponent() {
return <h1>Hey</h1>;
}
// eslint-disable-next-line react/jsx-no-bind
const { container } = renderWithRouterAndContext(<PrivateRoute component={MyComponent} />, {
location: '/hello',
context: { callbackPath: '/oauth', authenticated: true },
});
expect(container.innerHTML).toBe('<h1>Hey</h1>');
});
it('can invoke a render prop function', () => {
const { container } = renderWithRouterAndContext(
<PrivateRoute
render={({ history, location }: any) => <p>{${history.length} ${location.pathname}}</p>}
/>,
{ location: '/hello', context: { callbackPath: '/oauth', authenticated: true } },
);
expect(container.innerHTML).toBe('<p>1 /hello</p>');
});
});
describe('when unauthenticated', () => {
const authorize = jest.fn();
beforeEach(() => {
jest.clearAllMocks();
});
describe('for a matching path', () => {
it('checks user authorization and does not render anything', () => {
const { container } = renderWithRouterAndContext(
<PrivateRoute path="/hello">
<h1>Hello</h1>
</PrivateRoute>,
{
location: '/hello',
context: { callbackPath: '/oauth', authenticated: false, authorize },
},
);
expect(container.innerHTML).toBe('');
expect(authorize).toHaveBeenCalledWith(expect.objectContaining({ pathname: '/hello' }));
});
});
describe('for an OAuth callback path', () => {
it('does not check user authorization and does not render anything', () => {
const { container } = renderWithRouterAndContext(
<PrivateRoute path="/">
<h1>Hello</h1>
</PrivateRoute>,
{
location: '/oauth?code=xyz&state=foo',
context: { callbackPath: '/oauth', authenticated: false, authorize },
},
);
expect(container.innerHTML).toBe('');
expect(authorize).not.toHaveBeenCalled();
});
});
describe('for a non-matching path', () => {
it('does not check user authorization', () => {
renderWithRouterAndContext(
<PrivateRoute path="/hello">
<h1>Hello</h1>
</PrivateRoute>,
{ location: '/hi', context: { callbackPath: '/oauth', authenticated: false, authorize } },
);
expect(authorize).not.toHaveBeenCalled();
});
});
describe('when authentication is in progress', () => {
it('does not check user authorization', () => {
renderWithRouterAndContext(
<PrivateRoute path="/hello">
<h1>Hello</h1>
</PrivateRoute>,
{
location: '/hi',
context: {
callbackPath: '/hello',
authenticating: true,
authorize,
},
},
);
expect(authorize).not.toHaveBeenCalled();
});
});
});
});
使用新版本的react-router-dom v6和@types/react-router-dom v5.3.3,在解决了代码更改的一些错误后,它给出了“hello”路径未找到错误。
错误:
控制台.警告 没有路线匹配位置“/hello”
31 | );
32 | return {
> 33 | ...render(content, { wrapper } as any),
| ^
34 | history,
35 | };
36 | }
● PrivateRoute › 经过身份验证后 › 可以渲染子路由
expect(received).toBe(expected) // Object.is equality
Expected: "<h1>Hello</h1>"
Received: ""
44 | { location: '/hello', context: { callbackPath: '/oauth', authenticated: true } },
45 | );
> 46 | expect(container.innerHTML).toBe('<h1>Hello</h1>');
| ^
47 | });
48 |
49 | it('can render a component', () => {
at Object.<anonymous> (packages/auth/src/_tests_/PrivateRoute.test.tsx:46:35)
● PrivateRoute › 经过身份验证后 › 可以渲染组件
expect(received).toBe(expected) // Object.is equality
Expected: "<h1>Hey</h1>"
Received: ""
56 | context: { callbackPath: '/oauth', authenticated: true },
57 | });
> 58 | expect(container.innerHTML).toBe('<h1>Hey</h1>');
| ^
59 | });
60 |
61 | it('can invoke a render prop function', () => {
at Object.<anonymous> (packages/auth/src/_tests_/PrivateRoute.test.tsx:58:35)
● PrivateRoute › 经过身份验证后 › 可以调用 render prop 函数
expect(received).toBe(expected) // Object.is equality
Expected: "<p>1 /hello</p>"
Received: ""
66 | { location: '/hello', context: { callbackPath: '/oauth', authenticated: true } },
67 | );
> 68 | expect(container.innerHTML).toBe('<p>1 /hello</p>');
| ^
69 | });
70 | });
71 |
请给一些建议,先谢谢了
没有使用
Route
"/hello"
属性渲染 path
。 PrivateRoute
组件不会将 path
属性传递给它正在渲染的 Route
。不过,您的 PrivateRoute
组件过于复杂了。它应该简单地为嵌套路由呈现 Outlet
来呈现其 element
内容,或者在未经身份验证的用户的情况下,通常将其重定向到安全的不受保护的路由。
为未经身份验证的用户保留渲染逻辑
null
:
const PrivateRoute = () => {
const {
authorize,
authenticated,
authenticating,
callbackPath
} = useContext(AuthenticationContext);
const location = useLocation();
useEffect(() => {
if (!authenticated
&& !authenticating
&& location.pathname !== callbackPath
) {
const authorizeAsync = async () => {
authorize(location as unknown as URL);
};
authorizeAsync();
}
}, [authorize, location, authenticated, authenticating, callbackPath]);
return authenticated ? <Outlet /> : null;
}
应重构单元测试以将
PrivateRoute
渲染为布局路由组件。
import React from 'react';
import { render } from '@testing-library/react';
import { createMemoryHistory } from 'history';
import { unstable_HistoryRouter as Router } from 'react-router-dom';
import { PrivateRoute } from '..';
import AuthenticationContext, {
AuthenticationContextProps
} from '../AuthenticationContext';
describe('PrivateRoute', () => {
function renderWithRouterAndContext(
content: JSX.Element,
// eslint-disable-next-line @typescript-eslint/ban-types
{ location, context } = {} as { location: string; context: object },
) {
const history = createMemoryHistory({ initialEntries: [location] });
const defaultContext: AuthenticationContextProps = {
fetchToken: () => 'xyz' as any,
callbackPath: '/oauth',
setError: () => {},
authenticated: false,
authenticating: false,
authorize: () => {},
logout: () => {},
getToken: () => 'xyz',
};
const wrapper = ({ children }: { children: React.ReactNode }) => (
<AuthenticationContext.Provider value={{ ...defaultContext, ...context }}>
<Router history={history as any}>{children}</Router>
</AuthenticationContext.Provider>
);
return {
...render(content, { wrapper } as any),
history,
};
}
describe('when authenticated', () => {
it('can render children', () => {
const { container } = renderWithRouterAndContext(
<Routes>
<Route element={<PrivateRoute />}>
<Route path="/hello" element={<h1>Hello</h1>} />
</Route>
</Routes>,
{
location: '/hello',
context: { callbackPath: '/oauth', authenticated: true }
},
);
expect(container.innerHTML).toBe('<h1>Hello</h1>');
});
it('can render a component', () => {
function MyComponent() {
return <h1>Hey</h1>;
}
// eslint-disable-next-line react/jsx-no-bind
const { container } = renderWithRouterAndContext(
<Routes>
<Route element={<PrivateRoute />}>
<Route path="/hello" element={<MyComponent />} />
</Route>
</Routes>,
{
location: '/hello',
context: { callbackPath: '/oauth', authenticated: true },
}
);
expect(container.innerHTML).toBe('<h1>Hey</h1>');
});
});
describe('when unauthenticated', () => {
const authorize = jest.fn();
beforeEach(() => {
jest.clearAllMocks();
});
describe('for a matching path', () => {
it('checks user authorization and does not render anything', () => {
const { container } = renderWithRouterAndContext(
<Routes>
<Route element={<PrivateRoute />}>
<Route path="/hello" element={<h1>Hello</h1>} />
</Route>
</Routes>,
{
location: '/hello',
context: { callbackPath: '/oauth', authenticated: false, authorize },
},
);
expect(container.innerHTML).toBe('');
expect(authorize).toHaveBeenCalledWith(
expect.objectContaining({ pathname: '/hello' })
);
});
});
describe('for an OAuth callback path', () => {
it('does not check user authorization and does not render anything', () => {
const { container } = renderWithRouterAndContext(
<Routes>
<Route element={<PrivateRoute />}>
<Route path="/hello" element={<h1>Hello</h1>} />
</Route>
</Routes>,
{
location: '/oauth?code=xyz&state=foo',
context: { callbackPath: '/oauth', authenticated: false, authorize },
},
);
expect(container.innerHTML).toBe('');
expect(authorize).not.toHaveBeenCalled();
});
});
describe('for a non-matching path', () => {
it('does not check user authorization', () => {
renderWithRouterAndContext(
<Routes>
<Route path="/hi" element={<h1>Hi</h1>} />
<Route element={<PrivateRoute />}>
<Route path="/hello" element={<h1>Hello</h1>} />
</Route>
</Routes>,
{
location: '/hi',
context: { callbackPath: '/oauth', authenticated: false, authorize }
},
);
expect(authorize).not.toHaveBeenCalled();
});
});
describe('when authentication is in progress', () => {
it('does not check user authorization', () => {
renderWithRouterAndContext(
<Routes>
<Route path="/hi" element={<h1>Hi</h1>} />
<Route element={<PrivateRoute />}>
<Route path="/hello" element={<h1>Hello</h1>} />
</Route>
</Routes>,
{
location: '/hi',
context: {
callbackPath: '/hello',
authenticating: true,
authorize,
},
},
);
expect(authorize).not.toHaveBeenCalled();
});
});
});
});