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,};
