1、路由本质
路由本质就是对 url 进行改变和监听,来让某个 dom 节点显示对应的视图。
2、hash 与 history
2.1 hash模式
hash 的改变:我们可以通过 location 暴露出来的属性,直接去修改当前 URL 的 hash 值:
window.location.hash = 'index';
// 路径就会变为 xx网址/#index
hash 的感知:通过监听 “hashchange”事件,可以用 JS 来捕捉 hash 值的变化,进而决定我们页面内容是否需要更新:
// 监听hash变化,点击浏览器的前进后退会触发
window.addEventListener('hashchange', function(event){
// 根据 hash 的变化更新内容
},false)
2.2 history 模式
使用history.pushState 和 history.replaceState:
history.pushState(data[,title][,url]); // 向浏览历史中追加一条记录
// history.pushState({}, '', 'index')
// 路径就会变为 xx网址/index
history.replaceState(data[,title][,url]); // 修改(替换)当前页在浏览历史中的信息
在 history 模式下,我们可以通过监听 popstate 事件来达到我们的目的:
window.addEventListener('popstate', function(e) {
console.log(e)
});
3、实现react-router
3.1 使用结构
import { Router, Route, useHistory } from './Router';
const Foo = () => <div>foo</div>;
const Bar = () => <div>bar</div>;
const Links = () => {
const history = useHistory();
const go = (path: string) => {
// 触发存储的监听函数
const state = { name: path };
history.push(path, state);
};
return (
<div className="demo">
<button onClick={() => go('/foo')}>foo</button>
<button onClick={() => go('/bar')}>bar</button>
</div>
);
};
export default () => {
return (
<div>
<Router>
<Links />
<Route path="/foo">
<Foo />
</Route>
<Route path="/bar">
<Bar />
</Route>
</Router>
</div>
);
};
3.2 Router
Router 的核心原理就是通过 Provider 把 location 和 history 等路由关键信息传递给子组件,并且在路由发生变化的时候要让子组件可以感知到:
import React, { useState, useEffect, ReactNode } from 'react';
import { history, Location } from './history';
interface RouterContextProps {
history: typeof history;
location: Location;
}
export const RouterContext = React.createContext<RouterContextProps | null>(
null,
);
export const Router: React.FC = ({ children }) => {
const [location, setLocation] = useState(history.location);
useEffect(() => {
// 监听
const unlisten = history.listen(location => {
setLocation(location);
});
return unlisten;
}, []);
return (
<RouterContext.Provider value={{ history, location }}>
{children}
</RouterContext.Provider>
);
};
- 我们在组件初始化的时候利用 history.listen 监听了路由的变化,一旦路由发生改变,就会调用 setLocation 去更新 location 并且通过 Provider 传递给子组件。
- 并且这一步也会触发 Provider 的 value 值的变化,通知所有用 useContext 订阅了 history 和 location 的子组件去重新 render。
3.3 Route
Route 组件接受 path 和 children 两个 prop,本质上就决定了在某个路径下需要渲染什么组件,我们又可以通过 Router 的 Provider 传递下来的 location 信息拿到当前路径,所以这个组件需要做的就是判断当前的路径是否匹配,渲染对应组件。
import { useLocation } from "./hooks";
interface RouteProps {
path: string;
children: JSX.Element;
}
export const Route = ({ path, children }: RouteProps) => {
const { pathname } = useLocation();
const matched = path === pathname; // 匹配路由
if (matched) {
return children;
}
return null;
};
3.4 history
import { readOnly, parsePath } from './utils';
export interface history {
push(): void;
}
export type State = object | null;
export type Listener = (location: Location) => void;
export interface Path {
pathname: string;
search: string;
hash: string;
}
export interface Location<S extends State = State> extends Path {
state: S;
}
let location = getLocation();
function getLocation(): Location {
const { pathname, search, hash } = window.location;
return readOnly({
pathname,
search,
hash,
state: null,
});
}
function getNextLocation(to: string, state: State = null) {
return readOnly({
...parsePath(to),
state,
});
}
// 触发
function push(to: string, state?: State) {
location = getNextLocation(to, state);
listeners.forEach(fn => fn(location));
}
// 存储 history.listen 的回调函数
// 监听
let listeners: Listener[] = [];
function listen(fn: Listener) {
listeners.push(fn);
return function() {
listeners = listeners.filter(listener => listener !== fn);
};
}
// 用于处理浏览器前进后退操作
window.addEventListener('popstate', () => {
location = getLocation();
listeners.forEach(fn => fn(location));
});
export const history = {
get location() {
return location;
},
push,
listen,
};