浮动层的使用
通常的对话框使用:
- 对话框需要在父组件中声明,才能在子组件中控制是否显示。
- 给对话框传递参数智能由 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 路径去管理路由。
Link
定义一个导航链接,可以无刷新地改变页面 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 可以更好地定位。