咱们是袋鼠云数栈 UED 团队,致力于打造优良的一站式数据中台产品。咱们始终保持工匠精力,摸索前端路线,为社区积攒并流传教训价值。
本文作者:霜序
前言
在前一篇文章中,咱们具体的说了 react-router@3.x 降级到 @6.x 须要留神的问题以及变更的应用形式。
react-router 版本更新十分快,然而它的底层实现原理确是万变不离其中,在本文中会从前端路由登程到 react-router 原理总结与分享。
前端路由
在 Web 前端单页面利用 SPA(Single Page Application)中,路由是形容 URL 和 UI 之间的映射关系,这种映射是单向的,即 URL 的扭转会引起 UI 更新,无需刷新页面
如何实现前端路由
实现前端路由,须要解决两个外围问题
- 如何扭转 URL 却不引起页面刷新?
- 如何监测 URL 变动?
在前端路由的实现模式有两种模式,hash 和 history 模式,别离答复上述两个问题
hash 模式
- hash 是 url 中 hash(#) 及前面的局部,罕用锚点在页面内做导航,扭转 url 中的 hash 局部不会引起页面的刷新
- 通过 hashchange 事件监听 URL 的扭转。扭转 URL 的形式只有以下几种:通过浏览器导航栏的后退后退、通过
<a>
标签、通过window.location
,这几种形式都会触发hashchange
事件
history 模式
- history 提供了
pushState
和replaceState
两个办法,这两个办法扭转 URL 的 path 局部不会引起页面刷新 - 通过 popchange 事件监听 URL 的扭转。须要留神只在通过浏览器导航栏的后退后退扭转 URL 时会触发
popstate
事件,通过<a>
标签和pushState
/replaceState
不会触发popstate
办法。但咱们能够拦挡<a>
标签的点击事件和pushState
/replaceState
的调用来检测 URL 变动,也是能够达到监听 URL 的变动,绝对hashchange
显得稍微简单
JS 实现前端路由
基于 hash 实现
因为三种扭转 hash 的形式都会触发 hashchange
办法,所以只须要监听 hashchange
办法。须要在 DOMContentLoaded
后,解决一下默认的 hash 值
// 页面加载完不会触发 hashchange,这里被动触发一次 hashchange 事件,解决默认 hash
window.addEventListener('DOMContentLoaded', onLoad);
// 监听路由变动
window.addEventListener('hashchange', onHashChange);
// 路由变动时,依据路由渲染对应 UI
function onHashChange() {switch (location.hash) {
case '#/home':
routerView.innerHTML = 'This is Home';
return;
case '#/about':
routerView.innerHTML = 'This is About';
return;
case '#/list':
routerView.innerHTML = 'This is List';
return;
default:
routerView.innerHTML = 'Not Found';
return;
}
}
hash 实现 demo
基于 history 实现
因为 history 模式下,<a>
标签和 pushState
/replaceState
不会触发 popstate
办法,咱们须要对 <a>
的跳转和 pushState
/replaceState
做非凡解决。
- 对
<a>
作点击事件,禁用默认行为,调用pushState
办法并手动触发popstate
的监听事件 - 对
pushState
/replaceState
能够重写 history 的办法并通过派发事件可能监听对应事件
var _wr = function (type) {var orig = history[type];
return function () {var e = new Event(type);
e.arguments = arguments;
var rv = orig.apply(this, arguments);
window.dispatchEvent(e);
return rv;
};
};
// 重写 pushstate 事件
history.pushState = _wr('pushstate');
function onLoad() {routerView = document.querySelector('#routeView');
onPopState();
// 拦挡 <a> 标签点击事件默认行为
// 点击时应用 pushState 批改 URL 并更新手动 UI,从而实现点击链接更新 URL 和 UI 的成果。var linkList = document.querySelectorAll('a[href]');
linkList.forEach((el) =>
el.addEventListener('click', function (e) {e.preventDefault();
history.pushState(null, '', el.getAttribute('href'));
onPopState();}),
);
}
// 监听 pushstate 办法
window.addEventListener('pushstate', onPopState());
// 页面加载完不会触发 hashchange,这里被动触发一次 popstate 事件,解决默认 pathname
window.addEventListener('DOMContentLoaded', onLoad);
// 监听路由变动
window.addEventListener('popstate', onPopState);
// 路由变动时,依据路由渲染对应 UI
function onPopState() {switch (location.pathname) {
case '/home':
routerView.innerHTML = 'This is Home';
return;
case '/about':
routerView.innerHTML = 'This is About';
return;
case '/list':
routerView.innerHTML = 'This is List';
return;
default:
routerView.innerHTML = 'Not Found';
return;
}
}
history 实现 demo
React-Router 的架构
- history 库给 browser、hash 两种 history 提供了对立的 API,给到 react-router-dom 应用
- react-router 实现了路由的最外围能力。提供了
<Router>
、<Route>
等组件,以及配套 hook - react-router-dom 是对 react-router 更上一层封装。把 history 传入
<Router>
并初始化成<BrowserRouter>
、<HashRouter>
,补充了<Link>
这样给浏览器间接用的组件。同时把 react-router 间接导出,缩小依赖
History 实现
history
在上文中说到,BrowserRouter
应用 history 库提供的 createBrowserHistory
创立的 history
对象扭转路由状态和监听路由变动。
❓ 那么 history 对象须要提供哪些性能讷?
- 监听路由变动的
listen
办法以及对应的清理监听unlisten
办法 - 扭转路由的
push
办法
// 创立和治理 listeners 的办法
export const EventEmitter = () => {const events = [];
return {subscribe(fn) {events.push(fn);
return function () {events = events.filter((handler) => handler !== fn);
};
},
emit(arg) {events.forEach((fn) => fn && fn(arg));
},
};
};
BrowserHistory
const createBrowserHistory = () => {const EventBus = EventEmitter();
// 初始化 location
let location = {pathname: '/',};
// 路由变动时的回调
const handlePop = function () {
const currentLocation = {pathname: window.location.pathname,};
EventBus.emit(currentLocation); // 路由变动时执行回调
};
// 定义 history.push 办法
const push = (path) => {
const history = window.history;
// 为了放弃 state 栈的一致性
history.pushState(null, '', path);
// 因为 push 并不触发 popstate,咱们须要手动调用回调函数
location = {pathname: path};
EventBus.emit(location);
};
const listen = (listener) => EventBus.subscribe(listener);
// 解决浏览器的后退后退
window.addEventListener('popstate', handlePop);
// 返回 history
const history = {
location,
listen,
push,
};
return history;
};
对于 BrowserHistory 来说,咱们的解决须要减少一项,当咱们触发 push 的时候,须要手动告诉所有的监听者,因为 pushState 无奈触发 popState 事件,因而须要手动触发
HashHistory
const createHashHistory = () => {const EventBus = EventEmitter();
let location = {pathname: '/',};
// 路由变动时的回调
const handlePop = function () {
const currentLocation = {pathname: window.location.hash.slice(1),
};
EventBus.emit(currentLocation); // 路由变动时执行回调
};
// 不必手动执行回调,因为 hash 扭转会触发 hashchange 事件
const push = (path) => (window.location.hash = path);
const listen = (listener: Function) => EventBus.subscribe(listener);
// 监听 hashchange 事件
window.addEventListener('hashchange', handlePop);
// 返回的 history 上有个 listen 办法
const history = {
location,
listen,
push,
};
return history;
};
在实现 hashHistory 的时候,咱们只是对 hashchange 进行了监听,当该事件产生时,咱们获取到最新的 location 对象,在告诉所有的监听者 listener 执行回调函数
React-Router@6 丐版实现
- 绿色为 history 中的办法
- 紫色为 react-router-dom 中的办法
- 橙色为 react-router 中的办法
Router
🎗️ 基于 Context 的全局状态下发。Router 是一个“Provider-Consumer”模型
Router 做的事件很简略,接管navigator
和location
,应用 context 将数据传递上来,可能让子组件获取到相干的数据
function Router(props: IProps) {const { navigator, children, location} = props;
const navigationContext = React.useMemo(() => ({ navigator}), [navigator]);
const {pathname} = location;
const locationContext = React.useMemo(() => ({location: { pathname} }),
[pathname],
);
return (<NavigationContext.Provider value={navigationContext}>
<LocationContext.Provider value={locationContext} children={children} />
</NavigationContext.Provider>
);
}
HashRouter
基于不同的 history 调用 Router 组件。并且在 history 产生扭转的时候,监听 history,可能在 location 产生扭转的时候,执行回调扭转 location。
在上面的代码中,可能发现监听者为 setState
函数,在上述 hashHistory 中,如果咱们的 location 产生了扭转,会告诉到所有的监听者执行回调,也就是咱们这里的 setState
函数,即咱们可能拿到最新的 location 信息通过 LocationContext 传递给子组件,再去做对应的路由匹配
function HashRouter({children}) {let historyRef = React.useRef();
if (historyRef.current == null) {historyRef.current = createHashHistory();
}
let history = historyRef.current;
let [state, setState] = React.useState({location: history.location,});
React.useEffect(() => {const unListen = history.listen(setState);
return unListen;
}, [history]);
return (<Router children={children} location={state.location} navigator={history} />
);
}
Routes/Route
咱们可能发现在 v6.0 的版本 Route 组件只是一个工具人,并没有做任何事件。
function Route(_props: RouteProps): React.ReactElement | null {
invariant(
false,
`A <Route> is only ever to be used as the child of <Routes> element, ` +
`never rendered directly. Please wrap your <Route> in a <Routes>.`,
);
}
实际上解决所有逻辑的组件是 Routes,它外部实现了依据路由的变动,匹配出一个正确的组件。
const Routes = ({children}) => {return useRoutes(createRoutesFromChildren(children));
};
useRoutes 为整个 v6 版本的外围,分为路由上下文解析、路由匹配、路由渲染三个步骤
<Routes>
<Route path="/home" element={<Home />}>
<Route path="1" element={<Home1 />}>
<Route path="2" element={<Home2 />}></Route>
</Route>
</Route>
<Route path="/about" element={<About />}></Route>
<Route path="/list" element={<List />}></Route>
<Route path="/notFound" element={<NotFound />} />
<Route path="/navigate" element={<Navigate to="/notFound" />} />
</Routes>
上述 Routes 代码中,通过 createRoutesFromChildren 函数将 Route 组件结构化。能够把 <Route>
类型的 react element 对象,变成了一般的 route 对象构造,如下图
useRoutes
useRoutes 才是真正解决渲染关系的,其代码如下:
// 第一步:获取相干的 pathname
let location = useLocation();
let {matches: parentMatches} = React.useContext(RouteContext);
// 第二步:找到匹配的路由分支,将 pathname 和 Route 的 path 做匹配
const matches = matchRoutes(routes, location);
// 第三步:渲染真正的路由组件
const renderedMatches = _renderMatches(matches, parentMatches);
return renderedMatches;
matchRoutes
matchRoutes 中通过 pathname 和路由的 path 进行匹配
因为咱们在 Route 中定义的 path 都是相对路径,所以咱们在 matchRoutes 办法中,须要对 routes 对象遍历,对于 children 外面的 path 须要变成残缺的门路,并且须要将 routes 扁平化,不在应用嵌套构造
const flattenRoutes = (
routes,
branches = [],
parentsMeta = [],
parentPath = '',
) => {const flattenRoute = (route) => {
const meta = {
relativePath: route.path || '',
route,
};
const path = joinPaths([parentPath, meta.relativePath]);
const routesMeta = parentsMeta.concat(meta);
if (route.children?.length > 0) {flattenRoutes(route.children, branches, routesMeta, path);
}
if (route.path == null) {return;}
branches.push({path, routesMeta});
};
routes.forEach((route) => {flattenRoute(route);
});
return branches;
};
当咱们拜访 /#/home/1/2
的时候,取得的 matches 如下
咱们失去的 match 程序是从 Home → Home1 → Home2
\_renderMatches
\_renderMatches 才会渲染所有的 matches 对象
const _renderMatches = (matches, parentMatches = []) => {
let renderedMatches = matches;
return renderedMatches.reduceRight((outlet, match, index) => {let matches = parentMatches.concat(renderedMatches.slice(0, index + 1));
const getChildren = () => {
let children;
if (match.route.Component) {children = <match.route.Component />;} else if (match.route.element) {children = match.route.element;} else {children = outlet;}
return (
<RouteContext.Provider
value={{
outlet,
matches,
}}
>
{children}
</RouteContext.Provider>
);
};
return getChildren();}, null);
};
\_renderMatches 这段代码咱们可能明确 outlet 作为子路由是如何传递给父路由渲染的。matches 采纳从右往左的遍历程序,将上一项的返回值作为后一项的 outlet,那么子路由就作为 outlet 传递给了父路由
Outlet
实际上就是外部渲染 RouteContext 的 outlet 属性
function Outlet(props) {return useOutlet(props.context);
}
function useOutlet(context?: unknown) {let outlet = useContext(RouteContext).outlet; // 获取上一级 RouteContext 下面的 outlet
if (outlet) {
return (<OutletContext.Provider value={context}>{outlet}</OutletContext.Provider>
);
}
return outlet;
}
Link
在 Link 中,咱们应用 <a>
标签来做跳转,然而 a 标签会使页面从新刷新,所以须要阻止 a 标签的默认行为,调用 useNavigate 办法进行跳转
function Link({to, children, onClick}) {const navigate = useNavigate();
const handleClick = onClick
? onClick
: (event) => {event.preventDefault();
navigate(to);
};
return (<a href={to} onClick={handleClick}>
{children}
</a>
);
}
Hooks
function useLocation() {return useContext(LocationContext).location;
}
function useNavigate() {const { navigator} = useContext(NavigationContext);
const navigate = useCallback((to: string) => {navigator.push(to);
},
[navigator],
);
return navigate;
}
本文所有的代码链接可点击查看
参考链接
- react router v6 应用详解以及局部源码解析(新老版本比照)– 掘金
- 「React 进阶」react-router v6 通关指南 – 掘金
- 一文读懂 react-router 原理
最初
欢送关注【袋鼠云数栈 UED 团队】~
袋鼠云数栈 UED 团队继续为宽广开发者分享技术成绩,相继参加开源了欢送 star
- 大数据分布式任务调度零碎——Taier
- 轻量级的 Web IDE UI 框架——Molecule
- 针对大数据畛域的 SQL Parser 我的项目——dt-sql-parser
- 袋鼠云数栈前端团队代码评审工程实际文档——code-review-practices
- 一个速度更快、配置更灵便、应用更简略的模块打包器——ko