[]

基本使用

安装react-router

~ pnpm add react-router-dom
~ pnpm add @types/react-router-dom -D

路由页面

// src/pages/Home.tsx
const Home: FC = () => 
Home Page
; export default Home; // src/pages/About.tsx const About: FC = () =>
About Page
; export default About;

嵌套路由

// src/pages/User.tsx
type UserParams = { username: string };
const User: FC = () => {
    const { username } = useParams();
    return 
current user is {username}
; }; const UsersIndex: FC = () => { const match = useRouteMatch(); return (
  • User List
  • Pincman

Please select a user.

); }; export default UsersIndex;

链接菜单

// src/components/Menu.tsx
const Menu: FC = () => (
    
  • Home
  • About
  • Users
); export default Menu;

构建路由

// src/App.tsx
...
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
const App: FC = () => {
    return (
        
            
footer
); }; export default App;

路由模式

路由模式有真实地址hash两种,真实地址在生产环境下需要服务器软件URL重写(如: nginx)的支持,其生成的urlexample.com/some/path这样的格式.而hash则不需要,只要有CDN的地方扔上去就可以,其url看上去是这样example.com/#/your/page

真实地址通过BrowserRouter组件生成,hash模式通过HashRouter生成,他们可以通过使用Router这个底层组件自由切换,如下

安装history

history版本必须为4.x.x,因为react-router v5版本还不支持history v5

~ pnpm add history@"<5.0.0"
// src/App.tsx
const App: FC<{ hash?: boolean }> = ({ hash }) => {
    const historyCreator = hash ? createHashHistory() : createBrowserHistory();
    return (
        
        ...
        
    );
};

现在如果需要hash模式,可以这样

// src/main.tsx
ReactDOM.render(
    
        
    ,
    document.getElementById('root'),
);

当然也可以把hash选项放到状态管理(比如我们前面讲得context+userReducer或者后续将会讲到的redux,[mobx][]等)中手动更换,这样会更灵活方便,甚至还可以把这个配置放到localstorage中储存!

精确匹配

Route组件用于根据URL路径来匹配路由,Switch组件则用于匹配第一个路由,如果在Route列表外部不包装Switch则会匹配所有符合条件的路由,试一下把App.tsx中的Switch组件去除,然后访问/about,这时Home页面也会呈现,因为/about同时匹配了/

由于Switch总是匹配第一个符合的路由,那么如果我们把Home放到最前面会发生什么?这将导致我们访问任何其它路由都只会显示Home页面,这时候就需要为/路由添加添加一个exact参数来设置精确匹配了

// src/App.tsx
const App: FC<{ hash?: boolean }> = ({ hash }) => {
  ...
                
};

这时访问/则只会匹配Home页面了

路由导航

导航组件

一般导航用Link就可以了,但是在需要对匹配的链接添加一个CSS类的时候可以使用NavLink(如导航栏),具体使用如下


  React

路由跳转

Redirect组件用于设置跳转路由,如下

// src/App.tsx

  ...
    
        
    
    

在导航链接上添加/test

// src/components/Menu.tsx
...
  • redirect to home

编程式导航

通过useHistory这个hook可以获得history对象,通过这个对象可以实现push,replace等方式的手动跳转

const Menu: FC = () => {
    const history = useHistory();
    return (
        
...
); }; export default Menu;

代码分割

动态导入

通过动态导入的方式实现代码分割,可以使每个组件和页面只在需要的时候加载

动态导入需要安装@loadable/component这个库

~ pnpm add @loadable/component
~ pnpm add @types/loadable__component -D

现在可以尝试一下动态导入About页面

// src/App.tsx
const About = loadable(() => import('./pages/About'));

当我们从http://localhost:4000导航到/about时,开发者工具->networks中会显示一个异步加载的About.tsx页面,表示已经异步加载

对于每个页面都进行一次loadable的HOC包装是一件非常繁琐的事情,为了简单,可以一次性把所有需要的页面进行代码分割出去,并通过页面名称的字符串来动态加载页面,在vitejs下使用glob的方式实现,而cra或者webpack等可以直接使用import(./${props.page})的方式

vitejs通过const pages = import.meta.glob('../../pages/**/*.{tsx,jsx}')这样的语法可加载pages目录下的所有页面并最终生成如下代码

const modules = {
  './dir/foo.js': () => import('./dir/foo.js'),
  './dir/bar.js': () => import('./dir/bar.js')
}

我们在添加一个函数,作用是通过正则去除前缀路径,比如../../pages/和后缀,比如.tsx,.jsx等,就获得最终的动态导入对象

// src/components/Router/view.tsx
const getAsyncImports = (imports: Record Promise>, reg: RegExp) => {
    return Object.keys(imports)
        .map((key) => {
            const names = reg.exec(key);
            return Array.isArray(names) && names.length >= 2
                ? { [names[1]]: imports[key] }
                : undefined;
        })
        .filter((m) => !!m)
        .reduce((o, n) => ({ ...o, ...n }), []) as unknown as Record Promise>;
};
const pages = getAsyncImports(
    import.meta.glob('../../pages/**/*.{tsx,jsx}'),
    /..\/..\/pages\/([\w+.?/?]+).tsx|.jsx/i,
);

最终pages对象会像这样

如果生产环境下,后缀会是jsx

const modules = {
  Home: () => import('../../pages/Home.tsx')
  About: () => import('../../pages/About.tsx'),
  'users/Index': () => import('../../pages/users/Index.tsx')
}

现在loadable('Home')就会异步加载Home页面了

export const AsyncPage = ({ page }: { page: string }) => {
    const View = loadable(pages[page], {
        cacheKey: () => page.replaceAll('/', '-'),
    });
    return ;
};

修改App.tsx试一下



    


    


    

    

添加"加载中"组件

为了保证不会因为loadding组件的每次计算而导致页面总是被重新渲染,使用useMemo缓存

const Loading: FC = () => (
    
加载中
); export const AsyncPage: FC<{ page: string; loading?: JSX.Element }> = ({ page, loading }) => { const fallback = useMemo(() => loading ?? , []); const View = loadable(pages[page], { cacheKey: () => page.replaceAll('/', '-'), fallback, }); return ; };

以上代码看不出效果,可以使用p-min-delay来做一下延迟测试(请注意: 生产环境中不要添加延迟),并且需要设置一下超时

~ pnpm add p-min-delay promise-timeout
~ pnpm add @types/promise-timeout -D

代码

const ViewHoc = loadable(() => timeout(pMinDelay(pages[page](), 500), 12000), {
    cacheKey: () => page.replaceAll('/', '-'),
    fallback,
});

render函数

[react router][]加载组件页面有三种方案

  • component加载,如<Route path="/home" component={Home} />
  • children方式加载,如<Route path="/home"><Home /></Route>
  • render加载,如<Route path="/home" render={Home} />

通过componentrender加载,可像页面传递route props,而component每次都会创建新的渲染实例,所以一般会选择render函数来加载页面

首先定义一下类型

export interface PageOptions {
    page: string;
    loading?: JSX.Element;
}

export type PageProps<
    // 传递给Page的额外参数
    T extends Record = Record,
    Path extends string = string,
    Params extends { [K: string]: string | undefined } = ExtractRouteParams,
> = PageOptions &
    T & {
        route: RouteComponentProps;
    };

export interface RouteProps
    extends PageOptions,
        Omit {
    // 传递给Page的额外参数
    params?: Record;

改造AsyncPage

export const AsyncPage: FC = ({ page, loading, ...rest }) => {
    ...
    return ;
};

定义一个AsyncRoute作为异步路由组件

export const AsyncRoute: FC = (props) => {
    const { page, loading, params = {}, ...rest } = props;
    return (
         (
                
            )}
        />
    );
};

App.tsx中使用


    
    
    
    

同时也可以在User/Index页面中使用

提取单个用户详情组件为独立页面

// src/pages/users/Detail.tsx
type UserParams = { username: string };
const UserDetail: FC = () => {
    const { username } = useParams();
    return 
current user is {username}
; }; export default UserDetail;

提取用户列表组件为独立页面

// src/pages/users/List.tsx
const UserList: FC = () => {
    return 
User List
; }; export default UserList;

原来的用户页面修改为用户布局页面

// src/pages/users/Index.tsx
const UsersIndex: FC = () => {
    const match = useRouteMatch();
    const [count, setCount] = useState(0);
    return (
        
{count}
  • User List
  • Pincman
); }; export default UsersIndex;

缓存布局

运行上面的代码会发现每次在访问子页面时会造成Index重新渲染,表现为点击"增数"按钮count变成1,在点击User list或者Pincman链接,count重置为0,造成以上问题的原因是loadable会在每次访问时生成一个新的实例,解决办法就是上一节讲到的缓存hook-useMemo

单独使用useMemo会导致页面一直缓存无法渲染,所以需要一个判断重新渲染页面时机的变量,此变量可以是match.url,因为切换子页面时,父级布局页的match.url是不会变动的

export const AsyncPage: FC = ({ page, loading, ...rest }) => {
    const { url } = rest.route.match;
    const View = useMemo(
        () =>
            loadable(() => timeout(pMinDelay(pages[page](), 500), 12000), {
                cacheKey: () => page.replaceAll('/', '-'),
                fallback,
            }),
        [url],
    );
    ...
};

现在试试点击"增数"再切换子页面,已经不会再重新渲染父级了

为了后续的配置式路由可以更简便地实现,我们还可以为AsyncRouteAsyncRoute添加一个children

export const AsyncPage: FC = ({ page, loading, children, ...rest }) => {
    return {children};
};


export const AsyncRoute: FC = (props) => {
...
        render={(route) => (
            
                {children}
            
        )}
};

const UserIndex: FC = ({ children }) => {
    ...
    return (
        
    ...
{children}
); }; export default UserIndex;

现在可以在App.tsx中添加所有路由配置了


    
    
    
        
            
            
        
    
    

可用API

这部分介绍一些常用的[react router][]的API

因为本教程是Typescript编码,所以不必把所有的API参数和属性列出来,大家可以点开类型文件自己查看

Hooks及函数

修改render

有了Hooks之后不必要再往render里的组件传递route参数

// src/components/Router/types.ts
export type PageProps<
    // 传递给Page的额外参数
    T extends Record = Record,
> = PageOptions & T;
export const AsyncPage: FC = ({ page, loading, children, ...rest }) => {
    const { url } = useRouteMatch();
    ...
};

// src/components/Router/view.tsx
export const AsyncRoute: FC = (props) => {
    const { page, loading, params = {}, children, ...rest } = props;
    ...
                
                    {children}
                
};

useHistory

用于编程化导航,比如history.push("/home");

useLocation

获取当前URL的location对象,比如location.pathname

useRouteMatch

通过path参数与某个Routepath匹配,如果不传入参数,则与当前导航到的Route匹配,匹配后即可获取其被导航后的信息,比如match.url

matchPath

在组件渲染的生命周期之前获取match对象,其结果与渲染后使用useRouteMatch一样,用法如下

matchPath("/users/2", {
  path: "/users/:id",
  exact: true,
  strict: true
});
// 结果
//  {
//    isExact: true
//    params: {
//        id: "2"
//    }
//    path: "/users/:id"
//    url: "/users/2"
//  }

组件

  • Router: 底层路由构建器
  • BrowserRouter: html5 history api路由构建器
  • HashRouter: hash路由构建器
  • MemoryRouter: 不把URL写入浏览器地址栏而把历史记录保存在内存中的路由构建器,适用于测试或[react native][]等
  • Switch: 在路由列表中只渲染第一个与当前地址匹配的路由
  • Route: 定义路由,参数包含path(可以是个数组,对应多个地址),extra(精确匹配),render/component
  • Redirect: 跳转路由,部分参数与Route一样
  • Link: 通过组件模式导航路由
  • NavLink: 比Link多了个可以为当前链接激活时添加的CSS类
  • Prompt: 在用户离开页面之前跳出"是否离开"提示

其它知识

以下知识后续教程会讲解

  • 配置式路由
  • 认证和权限路由
  • 基于context的独立路由组件
  • 路由切换时的转场动画

以下知识自行查看官网示例

  • query: 通过Link组件中的query参数可指定query,如要获取query看[这里][https://reactrouter.com/web/example/query-parameters]
  • 弹出框: 想要点击一个路由可弹出一个modal,看[这里][https://reactrouter.com/web/example/modal-gallery]

发表评论

您的电子邮箱地址不会被公开。