SPA页面
单页面应用
单页面应用就是一个html,一次性加载js, css等资源,所有页面都在一个容器页面下,页面切换实质是组件的切换。
①react-router-dom和react-router和history库三者什么关系
react-router和history库都是react-router-dom的核心和集成。
react-router实现了Route、Router、Switch、Redirect等重要路由组件
而react-router-dom在history的基础上实现了BrowserRouter、HashRouter、Link、NavLink等组件。
使用方式(一)
import { Route, Switch, Router } from 'react-router-dom';
import { createBrowserHistory } from 'history'
const history = createBrowserHistory()
<Router history={history}>
<Switch>
<Route path='/' component={<A />} />
<Route path='/b' component={<B />} />
</Switch>
</Router>
history库做了什么
首先来讨论下,原始的window.history对象做了什么?
history路由执行history对象上的go、back、forward等方法,会自动触发window的popstate事件,并且向history栈内push一条历史记录。
hash路由利用锚点实现的,页面的hash内容发生变化时,会触发window的hashchange事件,并且向history栈内push一条历史记录。
以上,我们知道了不管是history路由还是hash路由,切换url的时候都会触发全局事件。
我们要想在单页面应用中,在页面跳转时匹配路由所对应的组件,就需要捕获到全局事件,添加回调,获取新的history对象,传递给Route组件。
history库:
createBrowserHistory方法:
const PopStateEvent = 'popstate'
const HashChangeEvent = 'hashchange' /* 这里简化了createBrowserHistory,列出了几个核心api及其作用 */
function createBrowserHistory(){
/* 全局history */
const globalHistory = window.history
/* 处理路由转换,记录了listens信息。 */
const transitionManager = createTransitionManager()
/* 改变location对象,通知组件更新 */
const setState = () => {
/* ... */
}
function go(n) {
globalHistory.go(n);
}
function goBack() {
go(-1);
}
function goForward() {
go(1);
}
/* 当path改变后,处理popstate变化的回调函数 */
const handlePopState = () => {
// 获取当前location,触发setState
/* ... */
}
/* history.push方法,改变路由,通过全局对象history.pushState改变url, 通知router触发更新,替换组件 */
const push=() => {
/*...*/
}
/* Router订阅location的变化 */
const listen=()=>{
/*...*/
}
return {
push,
listen,
go,
goBack
/* .... */ }
}
createBrowserHistory方法是产生记录路由历史状态的实例,是window.history对象的封装。
- history实例的go、goBack、goForward沿用了window.history对象的方法,会触发popstate事件,
history库监听了popstate事件,并在回调函数中获取新的location对象,并触发setState。 - 对于window.history对象上的pushState和replaceState方法,由于不会触发popstate事件,捕获不到由push和replace引起的url变化,
history库封装了push、replace方法,用原生的pushState和replaceState方法实现url跳转,并且获取新的location,触发setState。 setState怎么起作用?Router订阅了history实例上的location对象的变化事件,setState时会发布新的location对象给订阅者(Router)。
以上,就实现了history库的createBrowserHistory的功能。
history库总结:利用原生history对象实现无请求跳转,并通知Router(location)
Router组件做了什么
/* Router 作用是把 history location 等路由信息 传递下去 */
class Router extends React.Component {
static computeRootMatch(pathname) {
return { path: '/', url: '/', params: {}, isExact: pathname === '/' };
}
constructor(props) {
super(props);
this.state = {
location: props.history.location
};
//记录pending位置
//如果存在任何<Redirect>,则在构造函数中进行更改
//在初始渲染时。如果有,它们将在
//在子组件身上激活,我们可能会
//在安装<Router>之前获取一个新位置。
this._isMounted = false;
this._pendingLocation = null;
/* 此时的history,是history创建的history对象 */
if (!props.staticContext) {
/* 这里判断 componentDidMount 和 history.listen 执行顺序 然后把 location复制 ,防止组件重新渲染 */
this.unlisten = props.history.listen(location => {
/* 创建监听者 */
if (this._isMounted) {
this.setState({ location });
} else {
this._pendingLocation = location;
}
});
}
}
componentDidMount() {
this._isMounted = true;
if (this._pendingLocation) {
this.setState({ location: this._pendingLocation });
}
}
componentWillUnmount() {
/* 解除监听 */
if (this.unlisten) this.unlisten();
}
render() {
return (
/* 这里可以理解 react.createContext 创建一个 context上下文 ,保存router基本信息。children */
<RouterContext.Provider
children={this.props.children || null}
value={{
history: this.props.history,
location: this.state.location,
match: Router.computeRootMatch(this.state.location.pathname),
staticContext: this.props.staticContext
}}
/>
);
}
}
以上代码,可以看出,Router订阅了location对象的变化,在url发生变化时(location变化)通知Router,Router通过context,将location、history、match等信息向下传递。
Switch组件做了什么?
/* switch组件 */
class Switch extends React.Component {
render() {
return (
<RouterContext.Consumer>
{/* 含有 history location 对象的 context */}
{context => {
invariant(context, 'You should not use <Switch> outside a <Router>');
const location = this.props.location || context.location;
let element, match;
React.Children.forEach(this.props.children, child => {
if (match == null && React.isValidElement(child)) {
element = child;
// 子组件 也就是 获取 Route中的 path 或者 rediect 的 from
const path = child.props.path || child.props.from;
match = path
? matchPath(location.pathname, { ...child.props, path })
: context.match;
}
});
return match
? React.cloneElement(element, { location, computedMatch: match })
: null;
}}
</RouterContext.Consumer>
);
}
}
根据location中的pathname匹配switch下的多个子Route组件,找到第一个匹配的路由Route组件,将location、match作为props传递给它。
Route组件做了什么
/**
* The public API for matching a single path and rendering.
*/
class Route extends React.Component {
render() {
return (
<RouterContext.Consumer>
{context => {
/* router / route 会给予警告警告 */
invariant(context, "You should not use <Route> outside a <Router>");
// computedMatch 为 经过 swich处理后的 path
const location = this.props.location || context.location;
const match = this.props.computedMatch
? this.props.computedMatch // <Switch> already computed the match for us
: this.props.path
? matchPath(location.pathname, this.props)
: context.match;
const props = { ...context, location, match };
let { children, component, render } = this.props;
if (Array.isArray(children) && children.length === 0) {
children = null;
}
return (
<RouterContext.Provider value={props}>
{props.match
? children
? typeof children === "function"
? __DEV__
? evalChildrenDev(children, props, this.props.path)
: children(props)
: children
: component
? React.createElement(component, props)
: render
? render(props)
: null
: typeof children === "function"
? __DEV__
? evalChildrenDev(children, props, this.props.path)
: children(props)
: null}
</RouterContext.Provider>
);
}}
</RouterContext.Consumer>
);
}
}
总结:
Route取出context中的数据,将location、match等数据作为props传递给将要渲染的组件,所以我们在组件中才可以使用props.history访问history对象实例。
以上,其实我们已经了解了react路由的基本原理,利用原生的history对象实现不刷新跳转、记录历史状态,并利用发布订阅将更新后的路由信息,传递给Router,Router作为一个传递信息的中介,利用context向下传递,匹配唯一的路由并进行渲染替换,以此实现了不刷新跳转。
使用方式(二)
另外,我们好像还看到了BrowserRouter、HashRouter。
使用方式如下:
import { BrowserRouter, Switch, Route } from 'react-router-dom';
<BrowserRouter>
<Switch>
<Route path='/' component={<A />} />
<Route path='/b' component={<B />} />
</Switch>
</BrowserRouter>
BrowserRouter的原理是什么呢?
他是一个语法糖。
相当于
const history = createBrowserHistory()
<Router history={history}>
其他组件
Link
<Link to='/a'/>
原理:
function handleClick(){
// 阻止a默认跳转 调用history.push
}
<a href={to} onClick={handleClick}>
withRouter组件
只有直接路由组件可以在props中读到history路由对象的东西(Route组件实现的),其他非直接路由组件无法获取到(因为无法获取context,props中也没有),所以withRouter这个HOC(高阶组件)出现了。
功能:获取context中的内容,作为props传递给组件本身。