文章首发于语雀 @reach/router源码分析
欢迎来访,每周一篇原创文章,前端好多云这里不仅仅有前端,还有其他互联网上,随笔杂想等
未经允许严禁转载
@reach/router是一个react路由控件,是由前React-Router成员Ryan Florence开发的,对比一下react-router,就可得知:
- 尺寸更小
- 没有复杂的路由模式
- 特别好用
- 但不支持RN
接下来,就开始分析了。首页先讲述一下会有到的几个工具方法的作用。
resolve:合并目标url与基本url返回新url
pick:根据pathname找到匹配的路由组件
insertParams:将参数与当前路径匹配得到最后的路径
match:只匹配到uri的一条路径
有易到难,逐个分析各个API。首先讲述一下history。
API
history
顾名思义,这是用来保存浏览历史的。
@reach/router提供一个4个关于history的API,分别是globalHistory,navigate,createHistory,createMemorySource。
globalHistory是用来表示整个应用的全局history,是由函数createHistory执行生成的结果。navigate是它的一个方法,用来控制导航的。
let canUseDOM = !!(
typeof window !== "undefined" &&
window.document &&
window.document.createElement
);
// 通过canuseDOM判断是否可以使用浏览器的window,否则创建history所需要的属性
let getSource = () => {
return canUseDOM ? window : createMemorySource();
};
let globalHistory = createHistory(getSource());
let { navigate } = globalHistory;
从上面的代码中能看出,宿主环境如果是浏览器则用window.history,而如果不是,则自定义了hisotry。
createMemorySource
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];
// 模拟window关于history和location的一些方法
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;
}
}
};
};
createHistory
用来定义使用@reach/router的关于history方法。
React.Context
很重要,在@reach/router中大量使用到React.Context,具体的使用可以参考文档。下面介绍两个Context。
LocationContext
let LocationContext = createNamedContext("Location");
let Location = ({ children }) => (
<LocationContext.Consumer>
// 有contenxt则直接使用,否则使用组件LocationProvider
{context =>
context ? (
children(context)
) : (
<LocationProvider>{children}</LocationProvider>
)
}
</LocationContext.Consumer>
);
BaseContext
// 为嵌套路由器和链接设置baseuri和basepath
let BaseContext = createNamedContext("Base", { baseuri: "/", basepath: "/" });
// 创建组件Router,包裹了Location组件
let Router = props => (
<BaseContext.Consumer>
{baseContext => (
<Location>
{locationContext => (
<RouterImpl {...baseContext} {...locationContext} {...props} />
)}
</Location>
)}
</BaseContext.Consumer>
);
createNamedContext
const createNamedContext = (name, defaultValue) => {
// createContext来源于create-react-context
const Ctx = createContext(defaultValue);
Ctx.Consumer.displayName = `${name}.Consumer`;
Ctx.Provider.displayName = `${name}.Provider`;
return Ctx;
};
Link
// 如果没有forwardRef,那么forwardRef的作用就是单纯的输入输出
let { forwardRef } = React;
if (typeof forwardRef === "undefined") {
forwardRef = C => C;
}
let Link = forwardRef(({ innerRef, ...props }, ref) => (
<BaseContext.Consumer>
// BaseContext的默认值
{({ basepath, baseuri }) => (
// 上文的Location组件,因为没有contenxt,所以还是走了LocationProvider的那条路径
<Location>
{({ location, navigate }) => {
let { to, state, replace, getProps = k, ...anchorProps } = props;
// 得到最终的路径
let href = resolve(to, baseuri);
// 将字符串做为uri进行编码
let encodedHref = encodeURI(href);
let isCurrent = location.pathname === encodedHref;
// 是否部分相同
let isPartiallyCurrent = startsWith(location.pathname, encodedHref);
return (
<a
ref={ref || innerRef}
aria-current={isCurrent ? "page" : undefined}
{...anchorProps}
{...getProps({ isCurrent, isPartiallyCurrent, href, location })}
href={href}
onClick={event => {
// 有onClick,则执行onClick,否则通过navigate导航
if (anchorProps.onClick) anchorProps.onClick(event);
if (shouldNavigate(event)) {
event.preventDefault();
navigate(href, { state, replace });
}
}}
/>
);
}}
</Location>
)}
</BaseContext.Consumer>
));
Redirect
let Redirect = props => (
<BaseContext.Consumer>
{({ baseuri }) => (
<Location>
{locationContext => (
<RedirectImpl {...locationContext} baseuri={baseuri} {...props} />
)}
</Location>
)}
</BaseContext.Consumer>
);
function RedirectRequest(uri) {
this.uri = uri;
}
let redirectTo = to => {
throw new RedirectRequest(to);
};
class RedirectImpl extends React.Component {
// Support React < 16 with this hook
componentDidMount() {
let {
props: {
navigate,
to,
from,
replace = true,
state,
noThrow,
baseuri,
...props
}
} = this;
Promise.resolve().then(() => {
let resolvedTo = resolve(to, baseuri);
// 重定向到目标路径
navigate(insertParams(resolvedTo, props), { replace, state });
});
}
render() {
let {
props: { navigate, to, from, replace, state, noThrow, baseuri, ...props }
} = this;
let resolvedTo = resolve(to, baseuri);
// noThrow为false,可以用componentDidCatch捕获error,放置渲染失败
if (!noThrow) redirectTo(insertParams(resolvedTo, props));
return null;
}
}
Match
let Match = ({ path, children }) => (
<BaseContext.Consumer>
{({ baseuri }) => (
<Location>
{({ navigate, location }) => {
let resolvedPath = resolve(path, baseuri);
// 是否匹配根据当前路径与定义的match路径
let result = match(resolvedPath, location.pathname);
// 渲染
return children({
navigate,
location,
match: result
? {
...result.params,
uri: result.uri,
path
}
: null
});
}}
</Location>
)}
</BaseContext.Consumer>
);
下面说说最重要的两个组件,一个是Location,另一个则是Router。
Location
Typically you only have access to the location in Route Components, Location
provides the location anywhere in your app with a child render prop.
翻译一下:
通常你只能访问路由组件中的位置,Location
可以在你的应用程序的任何地方提供定位并渲染子组件。
let Location = ({ children }) => (
<LocationContext.Consumer>
{context =>
// 有contenxt则直接使用,否则使用组件LocationProvider渲染
// 只有在服务端渲染的情况下,会使用到第一种,直接渲染
context ? (
children(context)
) : (
<LocationProvider>{children}</LocationProvider>
)
}
</LocationContext.Consumer>
);
class LocationProvider extends React.Component {
static propTypes = {
history: PropTypes.object.isRequired
};
// 默认props有一个history属性
static defaultProps = {
history: globalHistory
};
state = {
context: this.getContext(),
refs: { unlisten: null }
};
// 自定义context
getContext() {
let {
props: {
history: { navigate, location }
}
} = this;
return { navigate, location };
}
// 捕获重定向的error,然后重定向到该error发生的uri
componentDidCatch(error, info) {
if (isRedirect(error)) {
let {
props: {
history: { navigate }
}
} = this;
navigate(error.uri, { replace: true });
} else {
throw error;
}
}
componentDidUpdate(prevProps, prevState) {
if (prevState.context.location !== this.state.context.location) {
this.props.history._onTransitionComplete();
}
}
componentDidMount() {
let {
state: { refs },
props: { history }
} = this;
history._onTransitionComplete();
// 监听history的popstate事件,并返回一个函数用来取消popstate事件
refs.unlisten = history.listen(() => {
Promise.resolve().then(() => {
// 重绘之前调用该回调,并且只有在未卸载的情况下,得到当前的Context
requestAnimationFrame(() => {
if (!this.unmounted) {
this.setState(() => ({ context: this.getContext() }));
}
});
});
});
}
componentWillUnmount() {
let {
state: { refs }
} = this;
// 已卸载
this.unmounted = true;
// 清除监听器
refs.unlisten();
}
render() {
let {
state: { context },
props: { children }
} = this;
return (
<LocationContext.Provider value={context}>
{typeof children === "function" ? children(context) : children || null}
</LocationContext.Provider>
);
}
}
Router
路由组件,不需要再来个Route,仅仅把需要渲染的组件放入其中,再搭配pathname属性就可以了。
// 创建组件Router,包裹了Location组件
let Router = props => (
<BaseContext.Consumer>
{baseContext => (
<Location>
{locationContext => (
<RouterImpl {...baseContext} {...locationContext} {...props} />
)}
</Location>
)}
</BaseContext.Consumer>
);
class RouterImpl extends React.PureComponent {
static defaultProps = {
primary: true
};
render() {
let {
location,
navigate,
basepath,
primary,
children,
baseuri,
component = "div",
...domProps
} = this.props;
let routes = React.Children.toArray(children).reduce((array, child) => {
// 判断并创建route
const routes = createRoute(basepath)(child);
if (routes instanceof Array) {
return array.concat(routes);
} else {
array.push(routes);
return array;
}
}, []);
let { pathname } = location;
// 根据pathname匹配route
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)
};
// 得到路由组件
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
// FocusWrapper的作用的,是使用div还是FocusHandler包裹渲染
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 }}>
<FocusWrapper {...wrapperProps}>{clone}</FocusWrapper>
</BaseContext.Provider>
);
} else {
return null;
}
}
}
至此,一些重要API已经分析好了,但还是有部分是遗漏的,有兴趣的还可以自行去看源码。
总结
大致来说,整个源码并不是很复杂,有耐心的小伙伴能看完。
说一下我的感受,在结合使用与源码解读的基础上,确实简单,使用简单,阅读简单。
但要去理解作者为什么这么去设计,为什么这样去写,这些还是需要我们去学习的。