reach router 的源码分析
Created: July 25, 2023 11:11 AM
前言
reach router 是一个常用的 react 的路由库。
它的实现原理并不复杂,主要是依赖 html5 的 window.history 的 pushState 和 replaceState 两个 api,因为他们能改变 history 内部的历史栈但是又能够不刷新页面。其使用了观察者模式,当用户通过其内置方法改变路由时,它会将对应监听的 listener 函数执行,listener 是个重置 react 组件状态的函数,从而实现组件重新渲染,以此达成无刷新页面但是能够更新页面的功能。当用户输入 url 进入页面时,其会解析当前对应的路径,并从当前的路由组件中找出匹配的组件渲染。
其实 reach router 的源码并不多,总计的有效也就 1000 多行,它仍然使用的是 legacy 版本的 react 编写,其实我们完全可以自己实现一个 react hook 版本的 reach router。
解析
官方源码有主要三个文件,index.js, history.js 和 utils.js
history.js
该文件用来存放关于 history 相关的库,最重要的是 createHistory 其会创建一个全局对象,并且提供 navigate 和 listen 方法,一个用来跳转,一个用来存放监听函数,代码比较简单具体可以看代码注释。
let createHistory = (source, options) => {
let listeners = [];
let location = getLocation(source);
let transitioning = false;
let resolveTransition = () => {};
return {
get location() {
return location;
},
get transitioning() {
return transitioning;
},
_onTransitionComplete() {
transitioning = false;
resolveTransition();
},
listen(listener) {
// 监听事件函数,LocationProvider每次初始化时都会监听。
listeners.push(listener);
let popstateListener = () => {
location = getLocation(source);
listener({ location, action: "POP" });
};
source.addEventListener("popstate", popstateListener);
return () => {
source.removeEventListener("popstate", popstateListener);
listeners = listeners.filter((fn) => fn !== listener);
};
},
navigate(to, { state, replace = false } = {}) {
debugger;
if (typeof to === "number") {
source.history.go(to);
} else {
state = { ...state, key: Date.now() + "" };
// try...catch iOS Safari limits to 100 pushState calls
try {
if (transitioning || replace) {
source.history.replaceState(state, null, to);
} else {
source.history.pushState(state, null, to);
}
} catch (e) {
source.location[replace ? "replace" : "assign"](to);
}
}
location = getLocation(source);
transitioning = true;
let transition = new Promise((res) => (resolveTransition = res));
//路由切换了后,执行当前的事件函数,从而达到刷新组件的目的。
listeners.forEach((listener) => listener({ location, action: "PUSH" }));
return transition;
},
};
};
因为可能当前的环境不一定是浏览器,也可能是 node 环境,因此这里还模拟了一个 history
let createMemorySource = (initialPath = "/") => {
let searchIndex = initialPath.indexOf("?");
let initialLocation = {
pathname:
searchIndex > -1 ? initialPath.substr(0, searchIndex) : initialPath,
search: searchIndex > -1 ? initialPath.substr(searchIndex) : "",
};
let index = 0;
let stack = [initialLocation];
let states = [null];
return {
get location() {
return stack[index];
},
addEventListener(name, fn) {},
removeEventListener(name, fn) {},
history: {
get entries() {
return stack;
},
get index() {
return index;
},
get state() {
return states[index];
},
pushState(state, _, uri) {
let [pathname, search = ""] = uri.split("?");
index++;
stack.push({ pathname, search: search.length ? `?${search}` : search });
states.push(state);
},
replaceState(state, _, uri) {
let [pathname, search = ""] = uri.split("?");
stack[index] = { pathname, search };
states[index] = state;
},
go(to) {
let newIndex = index + to;
if (newIndex < 0 || newIndex > states.length - 1) {
return;
}
index = newIndex;
},
},
};
};
index.js
这是 reach/router 的主体,里面的代码虽然不多,但是各种概念却是一点也不少。
首先是最常用的 Router,其外面套了一个 BaseContext.Consumer,这里主要是防止外部有 BaseContext.Provider 的话能够进行消费,这个主要是为了嵌套路由准备的,如果不理解可暂时不看,同样的 Location 也是同理。
// The main event, welcome to the show everybody.
let Router = (props) => (
<BaseContext.Consumer>
{(baseContext) => (
<Location>
{(locationContext) => (
<RouterImpl {...baseContext} {...locationContext} {...props} />
)}
</Location>
)}
</BaseContext.Consumer>
);
let Location = ({ children }) => (
<LocationContext.Consumer>
{(context) =>
context ? (
children(context)
) : (
<LocationProvider>{children}</LocationProvider>
)
}
</LocationContext.Consumer>
);
同时,在 LocationProvider 中,我们会在 history 全局对象里添加监听事件函数,这样子当用户点击 Link 或者使用 navigate 跳转的时候就能够刷新 Router.
class LocationProvider extends React.Component {
static propTypes = {
history: PropTypes.object.isRequired,
};
static defaultProps = {
history: globalHistory,
};
state = {
context: this.getContext(),
refs: { unlisten: null },
};
getContext() {
let {
props: {
history: { navigate, location },
},
} = this;
debugger;
return { navigate, location };
}
componentDidCatch(error, info) {
if (isRedirect(error)) {
let {
props: {
history: { navigate },
},
} = this;
navigate(error.uri, { replace: true });
} else {
throw error;
}
}
componentDidMount() {
let {
state: { refs },
props: { history },
} = this;
history._onTransitionComplete();
//组件挂载的时候添加监听函数,当触发的时候就会更新context并且触发setState,从而实现router组件刷新
refs.unlisten = history.listen(() => {
Promise.resolve().then(() => {
// TODO: replace rAF with react deferred update API when it's ready https://github.com/facebook/react/issues/13306
requestAnimationFrame(() => {
if (!this.unmounted) {
this.setState(() => ({ context: this.getContext() }));
}
});
});
});
}
// 省略
render() {
let {
state: { context },
props: { children },
} = this;
return (
<LocationContext.Provider value={context}>
{typeof children === "function" ? children(context) : children || null}
</LocationContext.Provider>
);
}
}
RouterImpl 组件如下,这里可以看到代码里都通过 React.Children.toArray 将 children 的都转成了数组形式(这是个非常有用的方法, 对于在 react 里,想用编程改变 jsx 的一些声明式的写法很有用),然后匹配出符合当前路由的 route,并进行渲染即可。
class RouterImpl extends React.PureComponent {
static defaultProps = {
primary: true,
};
render() {
let {
location,
navigate,
basepath,
primary,
children,
baseuri,
component = "div",
...domProps
} = this.props;
debugger;
let routes = React.Children.toArray(children).reduce((array, child) => {
const routes = createRoute(basepath)(child);
return array.concat(routes);
}, []);
let { pathname } = location;
// pick函数找出匹配的函数组件
let match = pick(routes, pathname);
if (match) {
let {
params,
uri,
route,
route: { value: element },
} = match;
// remove the /* from the end for child routes relative paths
basepath = route.default ? basepath : route.path.replace(/\*$/, "");
let props = {
...params,
uri,
location,
navigate: (to, options) => navigate(resolve(to, uri), options),
};
// 重新构建新的一个reactElement
let clone = React.cloneElement(
element,
props,
// 判断其内部是否还会有子节点,继续循环递归
element.props.children ? (
<Router location={location} primary={primary}>
{element.props.children}
</Router>
) : undefined
);
// using 'div' for < 16.3 support
let FocusWrapper = primary ? FocusHandler : component;
// don't pass any props to 'div'
let wrapperProps = primary
? { uri, location, component, ...domProps }
: domProps;
return (
<BaseContext.Provider
value={{ baseuri: uri, basepath, navigate: props.navigate }}
>
<FocusWrapper {...wrapperProps}>{clone}</FocusWrapper>
</BaseContext.Provider>
);
} else {
return null;
}
}
}
至于其中的 FocusWrapper 则是用来管理焦点状态的,属于细枝末节可以不管。另外还有一个 Match 组件,本质上实现和 Router 差不多,只是在功能上略有不同,不再赘述。
总结
整体代码量其实并不多,但是整体代码风格较乱,主要是早年使用 js 的原因。整体实现的功能很完善,麻雀虽小五脏俱全,也没有 react-router 各种乱七八糟的功能,该有的都有了并且没有多余的代码。不过确实比较老旧了,如果不需要它全部功能的话,没必要再去安装它,像这种库其实完全能自己实现一个。不过看看实现思路还是很不错的。