内容简介:router作为当前盛行的单页面应用必不可少的部分,今天我们就以React-Router V4为例,来解开他的神秘面纱。本文并不专注于讲解 Reacr-Router V4 的基础概念,可以前往官方文档了解更多基础知识本文以RRV4代指Reacr-Router V4RRV4依赖的
router作为当前盛行的单页面应用必不可少的部分,今天我们就以React-Router V4为例,来解开他的神秘面纱。本文并不专注于讲解 Reacr-Router V4 的基础概念,可以前往官方文档了解更多基础知识
本文以RRV4代指Reacr-Router V4
预备知识
RRV4依赖的 history 库
先来看几个问题
Q1. 为什么我们有时看到的写法是这样的
import { Switch, Route, Router, BrowserRouter, Link } from 'react-router-dom'; 复制代码
或是这样的?
import {Switch, Route, Router} from 'react-router'; import {BrowserRouter, Link} from 'react-router-dom'; 复制代码
react-router-dom和react-touter有什么关系和区别?
Q2. 为什么v4版本中支持div等标签的嵌套了?
Q3. Route 会在当前 url 与 path 属性值相符的时候渲染相关组件,他是如何做到的呢?
Q4. 为什么用Link组件,而不是a标签?
路由
进入RR V4之前,想想一下路由的作用,路由的作用就是同步url与其对应的回调函数。一般基于history,通过 history.pushstate 和 replacestate 方法修改url,通过 window.addEventListener('popstate', callback) 来监听前进后退,对于hash路由,通过window.location修改hash,通过 window.addEventListener('hashchange', callback) 监听变化
正文
为了便于理解原理先来一段关于RRV4的简单代码
<BrowserRouter> <div> <ul> <li><Link to="/">Home</Link></li> <li><Link to="/about">About</Link></li> </ul> <hr/> <Switch> <Route exact path="/" component={Home}/> <Route path="/about" component={About}/> </Switch> </div> </BrowserRouter> 复制代码
在看看Route的子组件的props
{ match: { path: "/", // 用来匹配的 path url: "/", // 当前的 URL params: {}, // 路径中的参数 isExact:true // 是否为严格匹配 pathname === "/" }, location: { hash: "" // hash key: "nyi4ea" // 唯一的key pathname: "/" // URL 中路径部分 search: "" // URL 参数 state: undefined // 路由跳转时传递的参数 state } history: {...} // history库提供 staticContext: undefined // 用于服务端渲染 } 复制代码
我们带着问题去看源码,发现 react-router-dom基于react-router ,Router, Route, Switch等都是引用的react-router,并且加入了Link,BrowserRouter,HashRouter组件,这里解释了Q1,react-router负责通用的路由管理, react-router-dom负责web,当然还有react-router-native负责rn的管理,我们从BrowserRouter开始看
BrowserRouter
rrv4的作者提倡Just Components 概念,BrowserRouter很简单,以组件的形式包装了Router,history传递下去,当然HashRouter也是同理
BrowserRouter源码
import { Router } from "react-router"; import { createBrowserHistory as createHistory } from "history"; class BrowserRouter extends React.Component { history = createHistory(this.props); render() { return <Router history={this.history} children={this.props.children} />; } } 复制代码
看完BrowserRouter,其实本质就是Router组件嘛,在下已经忍不住先去看react-router的Router源码了。
Router
Router作为Route的根组件,负责监听url的变化和传递数据(props), 这里使用了history.listen监听url,使用react context的Provider和Consumer模式,最初的数据来自history,并将 history, location, match, staticContext作为props传递
router源码+注释
// 构造props function getContext(props, state) { return { history: props.history, location: state.location, match: Router.computeRootMatch(state.location.pathname), staticContext: props.staticContext }; } class Router extends React.Component { static computeRootMatch(pathname) { return { path: "/", url: "/", params: {}, isExact: pathname === "/" }; } constructor(props) { super(props); this.state = { // browserRouter的props为history location: props.history.location }; this._isMounted = false; this._pendingLocation = null; // staticContext为true时,为服务器端渲染 // staticContext为false if (!props.staticContext) { // 监听listen,location改变触发 this.unlisten = props.history.listen(location => { // _isMounted为true表示经历过didmount,可以setState,防止在构造函数中setstate if (this._isMounted) { // 更新state location this.setState({ location }); } else { // 否则存储到_pendingLocation, 等到didmount再setState避免可报错 this._pendingLocation = location; } }); } } componentDidMount() { // 赋值为true,且不会再改变 this._isMounted = true; // 更新location if (this._pendingLocation) { this.setState({ location: this._pendingLocation }); } } componentWillUnmount() { // 取消监听 if (this.unlisten) this.unlisten(); } render() { const context = getContext(this.props, this.state); return ( <RouterContext.Provider children={this.props.children || null} value={context} /> ); } } 复制代码
rrv4中Router组件为context中的Pirover, children可以是任何div等元素,这里解释了问题Q2,react-router的v4版本直接推翻了之前的v2,v3版本,在v2,v3的版本中Router组件根据子组件的Route,生成全局的路由表,路由表中记录了path与UI组件的映射关系,Router监听path变化,当path变化时,根据新的path找出对应所需的所有UI组件,按一定层级将这些UI渲染出来.而在rrv4中作者提倡Just Components思想,这也符合react中一切皆组件的思想。
Route
在v4中,Route只是一个Consumer包装的react组件,不管path是什么,Route组件总会渲染,在Route内部判断请求路径与当前的path是否匹配,匹配会继续渲染Route中的children或者component或者render中的子组件,如果不匹配,渲染null
route源码+注释
function getContext(props, context) { const location = props.location || context.location; const match = props.computedMatch ? props.computedMatch // <Switch> already computed the match for us : props.path // <Route path='/xx' ... > ? matchPath(location.pathname, props) : context.match; // 默认 { path: "/", url: "/", params: {}, isExact: pathname === "/" } return { ...context, location, match }; } class Route extends React.Component { render() { return ( <RouterContext.Consumer> {context => { invariant(context, "You should not use <Route> outside a <Router>"); // 通过path生成props // this.props = {exact, path, component, children, render, computedMatch, ...others } // context = { history, location, staticContext, match } const props = getContext(this.props, context); // 结构Route的props let { children, component, render } = this.props; // 空数组用null代替 if (Array.isArray(children) && children.length === 0) { children = null; } if (typeof children === "function") { // 无状态组件时 children = children(props); if (children === undefined) { children = null; } } return ( <RouterContext.Provider value={props}> {children && !isEmptyChildren(children) // children && React.Children.count > 0 ? children : props.match // match为true,查找到了匹配的<Route ... > ? component ? React.createElement(component, props) //创建react组件,传递props{ ...context, location, match } : render ? render(props) // 执行render方法 : null : null} </RouterContext.Provider> // 优先级 children > component > render ); }} </RouterContext.Consumer> ); } } 复制代码
Route是一个组件,每一个Route都会监听自己context并执行重新的渲染,为子组件提供了新的props, props.match用来决定是否渲染component和render,props.match由matchPath生成, 这里我们不得不看一下matchPath这个很重要的方法,他决定当前Route的path与url的匹配。
matchPath
matchPath方法依赖 path-to-regexp 库, 举个小:chestnut:
// var keys = [] // var re = pathToRegexp('/foo/:bar', keys) // re = /^\/foo\/([^\/]+?)\/?$/i // keys = [{ name: 'bar', prefix: '/', delimiter: '/', optional: false, repeat: false, pattern: '[^\\/]+?' }] 复制代码
matchPath源码+注释
/** * Public API for matching a URL pathname to a path. * @param {*} pathname history.location.pathname * @param {*} options * 默认配置,是否全局匹配 exact,末尾加/ strict, 大小写 sensitive, path <Route path="/xx" ...> */ function matchPath(pathname, options = {}) { // use <Switch />, options = location.pathname if (typeof options === "string") options = { path: options }; const { path, exact = false, strict = false, sensitive = false } = options; // path存入paths数组 const paths = [].concat(path); return paths.reduce((matched, path) => { if (matched) return matched; // compilePath内部使用path-to-regexp库,并做了缓存处理 const { regexp, keys } = compilePath(path, { end: exact, strict, sensitive }); // 在pathname中查找path const match = regexp.exec(pathname); // 匹配失败 if (!match) return null; // 定义查找到的path为url const [url, ...values] = match; // 判断pathname与url是否相等 eg: '/' === '/home' const isExact = pathname === url; // 精准匹配时, 保证查找到的url === pathname if (exact && !isExact) return null; // 返回match object return { path, // the path used to match url: path === "/" && url === "" ? "/" : url, // the matched portion of the URL isExact, // whether or not we matched exactly params: keys.reduce((memo, key, index) => { memo[key.name] = values[index]; return memo; }, {}) }; }, null); } 复制代码
Switch
不知道看官老爷有没有注意到这里
这就是Switch组件渲染与位置匹配的第一个子组件Route或Redirect。Switch利用React.Children.forEach(this.props.children, child => {...})方法匹配第一个子组件, 如果匹配成功添加computedMatch props,props值为match。从而改变了matchPath的逻辑
switch部分源码
class Switch extends React.Component { render() { ...省略无关代码 let element, match; React.Children.forEach(this.props.children, child => { // child为react elemnet // match如果没有匹配到这为context.match if (match == null && React.isValidElement(child)) { element = child; // form用于<redirect form="..." ... > const path = child.props.path || child.props.from; // 匹配的match match = path ? matchPath(location.pathname, { ...child.props, path }) : context.match; // path undefind为默认mactch // note: path为undefined 时,会默认为'/' } }); return match // 添加computedMatch props为match ? React.cloneElement(element, { location, computedMatch: match }) : null; } } .... 复制代码
到这里我们了解了RRV4的基本工作流程和源码,解决了Q3,最后来看一下Link。
Link
Link组件的主要用于处理理用户通过点击锚标签进行跳转,之所以不用a标签是因为要避免每次用户切换路由时都进行页面的整体刷新,而是使用histoy库中的push和replace。解决Q4,当点击Link组件时,点击的是页面上渲染出来的 a 标签,通过preventDefault阻止默认行为,通过history的push或repalce跳转。
Link部分源码+注释
class Link extends React.Component { .... handleClick(event, context) { if (this.props.onClick) this.props.onClick(event); if ( !event.defaultPrevented && // onClick prevented default event.button === 0 && // 忽略不是左键的点击 (!this.props.target || this.props.target === "_self") && // let browser handle "target=_blank" etc. !isModifiedEvent(event) // ignore clicks with modifier keys ) { // 阻止默认行为 event.preventDefault(); const method = this.props.replace ? context.history.replace : context.history.push; method(this.props.to); } ... render() { .... return( <a {...rest} onClick={event => this.handleClick(event, context)} href={href} ref={innerRef} /> ) } '''' } 复制代码
到这里,本文结束,欢迎看官老爷们的到来。
以上所述就是小编给大家介绍的《React-router4(新版)源码浅析》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。