⚫ React Hooks 精进 14 - 浮动层的使用 & 路由管理


浮动层的使用

通常的对话框使用:

  • 对话框需要在父组件中声明,才能在子组件中控制是否显示。
  • 给对话框传递参数智能由 props 传入,意味着所有状态管理需要在更高级别的组件上。

处理对话框的误区

当这里是一个左右分栏布局的页面,在左侧的菜单栏由右侧列表的操作按钮,此时左侧组件和右侧组件共享一个对话框,那么此时就需要在 layout 这个层级上去设置对话框。

但是这种处理方式会有以下两个问题:

  • 语义隔离不明确:是两个本不相关的功能产生依赖
  • 难以扩展

需要思考的问题就是:一个实现业务逻辑的 Modal 究竟应该在哪个组件中去声明?又该如何进行交互?从而使让对话框相关的业务逻辑能够更加模块化

思路:使用全局状态管理所有对话框

对话框本质:独立于其他界面的独立功能。

所以我们可以给对话框定义一个全局唯一的 ID,通过 ID 去显示/隐藏对话框并为之传递参数。

预期的操作方式:

const modal = useNiceModal("user-info-modal");
modal.show(args);
modal.hide();

实现:创建组件和相关 API

首先是全局状态的管理(以 Redux 为例)

创建一个可以处理所有对话框状态的 reducer。

const modalReducer = (state = { hiding: {} }, action) => {
  switch (action.type) {
    case "nice-modal/show":
      return {
        ...state,
        [action.payload.modalId]: action.payload.args || true,
        hiding: {
          ...state.hiding,
          [action.payload.modalId]: false,
        },
      };
    case "nice-modal/hide":
      return action.payload.force
        ? {
            ...state,
            [action.payload.modalId]: false,
            hiding: { [action.payload.modalId]: false },
          }
        : { ...state, hiding: { [action.payload.modalId]: true } };
    default:
      return state;
  }
};
// action creators
function showModal(modalId, args) {
  return {
    type: "nice-modal/show",
    payload: {
      modalId,
      args,
    },
  };
}

function hideModal(modalId, force) {
  return {
    type: "nice-modal/hide",
    payload: {
      modalId,
      force,
    },
  };
}

创建一个 Hook,保证逻辑重用:

const modalCallbacks = {};
const useNiceModal = (modalId) => {
  const dispatch = useDispatch();
  const show = useCallback(
    (args) => {
      return new Promise((resolve) => {
        modalCallbacks[modalId] = resolve;
        dispatch(showModal(modalId, args));
      });
    },
    [dispatch, modalId]
  );
  const resolve = useCallback(
    (args) => {
      if (modalCallbacks[modalId]) {
        modalCallbacks[modalId](args);
        delete modalCallbacks[modalId];
      }
    },
    [modalId]
  );

  const hide = useCallback(
    (force) => {
      dispatch(hideModal(modalId, force));
      delete modalCallbacks[modalId];
    },
    [dispatch, modalId]
  );

  const args = useSelector((s) => s[modalId]);
  const hiding = useSelector((s) => s.hiding[modalId]);

  return useMemo(
    () => ({ args, hiding, visible: !!args, show, hide, resolve }),
    [args, hide, show, resolve, hiding]
  );
};

// 封装一个通用对话框
function NiceModal({ id, children, ...rest }) {
  const modal = useNiceModal(id);
  return (
    <Modal
      onCancel={() => modal.hide()}
      onOk={() => modal.hide()}
      afterClose={() => modal.hide(true)}
      visible={!modal.hiding}
      {...rest}
    >
      {children}
    </Modal>
  );
}

最后可以使用容器模式,在对话框关闭时直接返回 null 从而以保证性能。

export const createNiceModal = (modalId, Comp) => {
  return (props) => {
    const { visible, args } = useNiceModal(modalId);
    if (!visible) return null;
    return <Comp {...args} {...props} />;
  };
};

最后就是调用操作:

import { Button } from "antd";
import { createStore } from "redux";
import { Provider } from "react-redux";
import NiceModal, {
  createNiceModal,
  useNiceModal,
  modalReducer,
} from "./NiceModal";

// redux store
const store = createStore(modalReducer);

const MyModal = createNiceModal("my-modal", () => {
  return (
    <NiceModal id="my-modal" title="Nice Modal">
      Hello NiceModal!
    </NiceModal>
  );
});

function MyModalExample() {
  const modal = useNiceModal("my-modal");
  return (
    <>
      <Button type="primary" onClick={() => modal.show()}>
        Show Modal
      </Button>
      <MyModal />
    </>
  );
}
export default () => {
  return (
    <Provider store={store}>
      <h1>Nice Modal</h1>
      <MyModalExample />
    </Provider>
  );
};

处理对话框的返回值

  • 对话框可能需要返回值给调用者

所以可以利用 Promise 来完成:

const modal = useNiceModal("my-modal");
modal.show(args).then((res) => {});

// 在useNiceModal内
const show = useCallback(
  (args) => {
    return new Promise((resolve) => {
      modalCallbacks[modalId] = resolve;
      dispatch(showModal(modalId, args));
    });
  },
  [dispatch, modalId]
);
const resolve = useCallback(
  (args) => {
    if (modalCallbacks[modalId]) {
      modalCallbacks[modalId](args);
      delete modalCallbacks[modalId];
    }
  },
  [modalId]
);

思考题

如果是基于 Context 该如何实现 NiceModal?

路由管理

  • 路由提供了按页面去组织整个应用程序的能力
  • URL(Uniform Resource Locator),表明 URL 是用于唯一的定位某个资源的

路由的工作原理:实现一个简单的路由机制

所谓的页面切换就是一个页面局部内容的组件切换。

  • 在服务器渲染的页面,URL 的变化都是全部页面内容的切换
  • 前段路由管理中,则一般只是主内容区域的变化

实现路由机制的核心逻辑:根据 URL 路径这个状态来决定内容区域显示什么组件

const MyRouter = ({ children }) => {
  const routes = _.keyBy(
    children.map((c) => c.props),
    "path"
  );
  const [hash] = useHash();
  const Page = routes[hash.replace("#", "")]?.component;
  return Page ? <Page /> : "Not found.";
};

const Route = () => null;

其中空组件 Route 可以接收路由的具体参数 path 和 component 从而以声明式的方式去定义路由。

<MyRouter>
  <Route path="page1" component={Page1} />
  <Route path="page2" component={Page2} />
  <Route path="page3" component={Page3} />
  <Route path="page4" component={Page4} />
</MyRouter>

使用 React Router

实际项目中需要考虑:路由嵌套,URL 模式匹配,参数提取等。

React Router 不仅支持浏览器还支持 React Native 以及一些用 Web 实现的移动 App。

通常在开发中使用的 react-router-dom 是支持浏览器的模块。

BrowserRouter

表示用标准的 URL 路径去管理路由。

定义一个导航链接,可以无刷新地改变页面 URL。

Route

定义一条路由规则,可以指定匹配路径和渲染内容。

Switch

可以保证只有第一个匹配到的路由才会被渲染。

使用嵌套路由:实现二级导航

  • 能够模糊匹配
  • Route 能够嵌套使用

在 URL 中保存页面状态

利用 React Router 的参数功能来定义一条路由。

<Route path="/tabs/:activeTab" component={TabsPage} />}

路由层面实现权限控制

利用前端路由的动态性。

import { useState } from "react";
import { Button } from "antd";
import { Route, Link } from "react-router-dom";

const Page1 = () => "Page 1";
const Page2 = () => "Page 2";
const UnauthedPage = () => (
  <span style={{ color: "red" }}>Unauthorized, please log in first.</span>
);
export default () => {
  const [loggedIn, setLoggedIn] = useState(false);
  const routes = loggedIn
    ? [
        {
          path: "/15/RouterAuth",
          component: Page1,
        },
        {
          path: "/15/RouterAuth/page1",
          component: Page1,
        },
        {
          path: "/15/RouterAuth/page2",
          component: Page2,
        },
      ]
    : [{ path: "/15/RouterAuth", component: UnauthedPage }];

  return (
    <div>
      <h1>Router Auth</h1>
      <Button
        type={loggedIn ? "primary" : ""}
        onClick={() => setLoggedIn((v) => !v)}
      >
        {loggedIn ? "Log Out" : "Log In"}
      </Button>

      <div className="exp-15-router-auth">
        <div className="exp-15-sider">
          <Link to="/15/RouterAuth/page1">Page 1</Link>
          <Link to="/15/RouterAuth/page2">Page 2</Link>
        </div>
        <div className="exp-15-page-container">
          {routes.map((r) => (
            <Route path={r.path} component={r.component} />
          ))}
        </div>
      </div>
    </div>
  );
};

思考题

为什么 URL 存储状态可以简化页面之间的交互?

因为如果子页面需要来自父页面的参数时,通过 URL 可以更好地定位。


文章作者: 阿汪同学
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 阿汪同学 !
评论
  目录