1、路由本质

路由本质就是对 url 进行改变和监听,来让某个 dom 节点显示对应的视图。

2、hash 与 history

2.1 hash模式

hash 的改变:我们可以通过 location 暴露出来的属性,直接去修改当前 URL 的 hash 值:

  1. window.location.hash = 'index';
  2. // 路径就会变为 xx网址/#index

hash 的感知:通过监听 “hashchange”事件,可以用 JS 来捕捉 hash 值的变化,进而决定我们页面内容是否需要更新:

  1. // 监听hash变化,点击浏览器的前进后退会触发
  2. window.addEventListener('hashchange', function(event){
  3. // 根据 hash 的变化更新内容
  4. },false)

2.2 history 模式

使用history.pushState 和 history.replaceState:

  1. history.pushState(data[,title][,url]); // 向浏览历史中追加一条记录
  2. // history.pushState({}, '', 'index')
  3. // 路径就会变为 xx网址/index
  1. history.replaceState(data[,title][,url]); // 修改(替换)当前页在浏览历史中的信息

在 history 模式下,我们可以通过监听 popstate 事件来达到我们的目的:

  1. window.addEventListener('popstate', function(e) {
  2. console.log(e)
  3. });

3、实现react-router

3.1 使用结构

  1. import { Router, Route, useHistory } from './Router';
  2. const Foo = () => <div>foo</div>;
  3. const Bar = () => <div>bar</div>;
  4. const Links = () => {
  5. const history = useHistory();
  6. const go = (path: string) => {
  7. // 触发存储的监听函数
  8. const state = { name: path };
  9. history.push(path, state);
  10. };
  11. return (
  12. <div className="demo">
  13. <button onClick={() => go('/foo')}>foo</button>
  14. <button onClick={() => go('/bar')}>bar</button>
  15. </div>
  16. );
  17. };
  18. export default () => {
  19. return (
  20. <div>
  21. <Router>
  22. <Links />
  23. <Route path="/foo">
  24. <Foo />
  25. </Route>
  26. <Route path="/bar">
  27. <Bar />
  28. </Route>
  29. </Router>
  30. </div>
  31. );
  32. };

3.2 Router

Router 的核心原理就是通过 Provider 把 location 和 history 等路由关键信息传递给子组件,并且在路由发生变化的时候要让子组件可以感知到:

  1. import React, { useState, useEffect, ReactNode } from 'react';
  2. import { history, Location } from './history';
  3. interface RouterContextProps {
  4. history: typeof history;
  5. location: Location;
  6. }
  7. export const RouterContext = React.createContext<RouterContextProps | null>(
  8. null,
  9. );
  10. export const Router: React.FC = ({ children }) => {
  11. const [location, setLocation] = useState(history.location);
  12. useEffect(() => {
  13. // 监听
  14. const unlisten = history.listen(location => {
  15. setLocation(location);
  16. });
  17. return unlisten;
  18. }, []);
  19. return (
  20. <RouterContext.Provider value={{ history, location }}>
  21. {children}
  22. </RouterContext.Provider>
  23. );
  24. };
  • 我们在组件初始化的时候利用 history.listen 监听了路由的变化,一旦路由发生改变,就会调用 setLocation 去更新 location 并且通过 Provider 传递给子组件。
  • 并且这一步也会触发 Provider 的 value 值的变化,通知所有用 useContext 订阅了 history 和 location 的子组件去重新 render。

3.3 Route

Route 组件接受 path 和 children 两个 prop,本质上就决定了在某个路径下需要渲染什么组件,我们又可以通过 Router 的 Provider 传递下来的 location 信息拿到当前路径,所以这个组件需要做的就是判断当前的路径是否匹配,渲染对应组件。

  1. import { useLocation } from "./hooks";
  2. interface RouteProps {
  3. path: string;
  4. children: JSX.Element;
  5. }
  6. export const Route = ({ path, children }: RouteProps) => {
  7. const { pathname } = useLocation();
  8. const matched = path === pathname; // 匹配路由
  9. if (matched) {
  10. return children;
  11. }
  12. return null;
  13. };

3.4 history

  1. import { readOnly, parsePath } from './utils';
  2. export interface history {
  3. push(): void;
  4. }
  5. export type State = object | null;
  6. export type Listener = (location: Location) => void;
  7. export interface Path {
  8. pathname: string;
  9. search: string;
  10. hash: string;
  11. }
  12. export interface Location<S extends State = State> extends Path {
  13. state: S;
  14. }
  15. let location = getLocation();
  16. function getLocation(): Location {
  17. const { pathname, search, hash } = window.location;
  18. return readOnly({
  19. pathname,
  20. search,
  21. hash,
  22. state: null,
  23. });
  24. }
  25. function getNextLocation(to: string, state: State = null) {
  26. return readOnly({
  27. ...parsePath(to),
  28. state,
  29. });
  30. }
  31. // 触发
  32. function push(to: string, state?: State) {
  33. location = getNextLocation(to, state);
  34. listeners.forEach(fn => fn(location));
  35. }
  36. // 存储 history.listen 的回调函数
  37. // 监听
  38. let listeners: Listener[] = [];
  39. function listen(fn: Listener) {
  40. listeners.push(fn);
  41. return function() {
  42. listeners = listeners.filter(listener => listener !== fn);
  43. };
  44. }
  45. // 用于处理浏览器前进后退操作
  46. window.addEventListener('popstate', () => {
  47. location = getLocation();
  48. listeners.forEach(fn => fn(location));
  49. });
  50. export const history = {
  51. get location() {
  52. return location;
  53. },
  54. push,
  55. listen,
  56. };

4、源码地址

https://github.com/linhexs/mini-react-router