ReactRouter的实现
ReactRouter的实现
ReactRouter
是React
的核心组件,主要是作为React
的路由管理器,保持UI
与URL
同步,其拥有简单的API
与强大的功能例如代码缓冲加载、动态路由匹配、以及建立正确的位置过渡处理等。
描述
React Router
是建立在history
对象之上的,简而言之一个history
对象知道如何去监听浏览器地址栏的变化,并解析这个URL
转化为location
对象,然后router
使用它匹配到路由,最后正确地渲染对应的组件,常用的history
有三种形式: Browser History
、Hash History
、Memory History
。
Browser History
Browser History
是使用React Router
的应用推荐的history
,其使用浏览器中的History
对象的pushState
、replaceState
等API
以及popstate
事件等来处理URL
,其能够创建一个像https://www.example.com/path
这样真实的URL
,同样在页面跳转时无须重新加载页面,当然也不会对于服务端进行请求,当然对于history
模式仍然是需要后端的配置支持,用以支持非首页的请求以及刷新时后端返回的资源,由于应用是个单页客户端应用,如果后台没有正确的配置,当用户在浏览器直接访问URL
时就会返回404
,所以需要在服务端增加一个覆盖所有情况的候选资源,如果URL
匹配不到任何静态资源时,则应该返回同一个index.html
应用依赖页面,例如在Nginx
下的配置。
location / {
try_files $uri $uri/ /index.html;
}
Hash History
Hash
符号即#
原本的目的是用来指示URL
中指示网页中的位置,例如https://www.example.com/index.html#print
即代表example
的index.html
的print
位置,浏览器读取这个URL
后,会自动将print
位置滚动至可视区域,通常使用<a>
标签的name
属性或者<div>
标签的id
属性指定锚点。
通过window.location.hash
属性能够读取锚点位置,可以为Hash
的改变添加hashchange
监听事件,每一次改变Hash
,都会在浏览器的访问历史中增加一个记录,此外Hash
虽然出现在URL
中,但不会被包括在HTTP
请求中,即#
及之后的字符不会被发送到服务端进行资源或数据的请求,其是用来指导浏览器动作的,对服务器端没有效果,因此改变Hash
不会重新加载页面。
ReactRouter
的作用就是通过改变URL
,在不重新请求页面的情况下,更新页面视图,从而动态加载与销毁组件,简单的说就是,虽然地址栏的地址改变了,但是并不是一个全新的页面,而是之前的页面某些部分进行了修改,这也是SPA
单页应用的特点,其所有的活动局限于一个Web
页面中,非懒加载的页面仅在该Web
页面初始化时加载相应的HTML
、JavaScript
、CSS
文件,一旦页面加载完成,SPA
不会进行页面的重新加载或跳转,而是利用JavaScript
动态的变换HTML
,默认Hash
模式是通过锚点实现路由以及控制组件的显示与隐藏来实现类似于页面跳转的交互。
Memory History
Memory History
不会在地址栏被操作或读取,这就可以解释如何实现服务器渲染的,同时其也非常适合测试和其他的渲染环境例如React Native
,和另外两种History
的一点不同是我们必须创建它,这种方式便于测试。
const history = createMemoryHistory(location);
实现
我们来实现一个非常简单的Browser History
模式与Hash History
模式的实现,因为H5
的pushState
方法不能在本地文件协议file://
运行,所以运行起来需要搭建一个http://
环境,使用webpack
、Nginx
、Apache
等都可以,回到Browser History
模式路由,能够实现history
路由跳转不刷新页面得益与H5
提供的pushState()
、replaceState()
等方法以及popstate
等事件,这些方法都是也可以改变路由路径,但不作页面跳转,当然如果在后端不配置好的情况下路由改编后刷新页面会提示404
,对于Hash History
模式,我们的实现思路相似,主要在于没有使用pushState
等H5
的API
,以及监听事件不同,通过监听其hashchange
事件的变化,然后拿到对应的location.hash
更新对应的视图。
<!-- Browser History -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Router</title>
</head>
<body>
<ul>
<li><a href="/home">home</a></li>
<li><a href="/about">about</a></li>
<div id="routeView"></div>
</ul>
</body>
<script>
function Router() {
this.routeView = null; // 组件承载的视图容器
this.routes = Object.create(null); // 定义的路由
}
// 绑定路由匹配后事件
Router.prototype.route = function (path, callback) {
this.routes[path] = () => this.routeView.innerHTML = callback() || "";
};
// 初始化
Router.prototype.init = function(root, rootView) {
this.routeView = rootView; // 指定承载视图容器
this.refresh(); // 初始化即刷新视图
root.addEventListener("click", (e) => { // 事件委托到root
if (e.target.nodeName === "A") {
e.preventDefault();
history.pushState(null, "", e.target.getAttribute("href"));
this.refresh(); // 触发即刷新视图
}
})
// 监听用户点击后退与前进
// pushState与replaceState不会触发popstate事件
window.addEventListener("popstate", this.refresh.bind(this), false);
};
// 刷新视图
Router.prototype.refresh = function () {
let path = location.pathname;
console.log("refresh", path);
if(this.routes[path]) this.routes[path]();
else this.routeView.innerHTML = "";
};
window.Router = new Router();
Router.route("/home", function() {
return "home";
});
Router.route("/about", function () {
return "about";
});
Router.init(document, document.getElementById("routeView"));
</script>
</html>
<!-- Hash History -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Router</title>
</head>
<body>
<ul>
<li><a href="#/home">home</a></li>
<li><a href="#/about">about</a></li>
<div id="routeView"></div>
</ul>
</body>
<script>
function Router() {
this.routeView = null; // 组件承载的视图容器
this.routes = Object.create(null); // 定义的路由
}
// 绑定路由匹配后事件
Router.prototype.route = function (path, callback) {
this.routes[path] = () => this.routeView.innerHTML = callback() || "";
};
// 初始化
Router.prototype.init = function(root, rootView) {
this.routeView = rootView; // 指定承载视图容器
this.refresh(); // 初始化触发
// 监听hashchange事件用以刷新
window.addEventListener("hashchange", this.refresh.bind(this), false);
};
// 刷新视图
Router.prototype.refresh = function () {
let hash = location.hash;
console.log("refresh", hash);
if(this.routes[hash]) this.routes[hash]();
else this.routeView.innerHTML = "";
};
window.Router = new Router();
Router.route("#/home", function() {
return "home";
});
Router.route("#/about", function () {
return "about";
});
Router.init(document, document.getElementById("routeView"));
</script>
</html>
分析
我们可以看一下ReactRouter
的实现,commit id
为eef79d5
,TAG
是4.4.0
,在这之前我们需要先了解一下history
库,history
库,是ReactRouter
依赖的一个对window.history
加强版的history
库,其中主要用到的有match
对象表示当前的URL
与path
的匹配的结果,location
对象是history
库基于window.location
的一个衍生。
ReactRouter
将路由拆成了几个包: react-router
负责通用的路由逻辑,react-router-dom
负责浏览器的路由管理,react-router-native
负责react-native
的路由管理。
我们以BrowserRouter
组件为例,BrowserRouter
在react-router-dom
中,它是一个高阶组件,在内部创建一个全局的history
对象,可以监听整个路由的变化,并将history
作为props
传递给react-router
的Router
组件,Router
组件再会将这个history
的属性作为context
传递给子组件。
// packages\react-router-dom\modules\HashRouter.js line 10
class BrowserRouter extends React.Component {
history = createHistory(this.props);
render() {
return <Router history={this.history} children={this.props.children} />;
}
}
接下来我们到Router
组件,Router
组件创建了一个React Context
环境,其借助context
向Route
传递context
,这也解释了为什么Router
要在所有Route
的外面。在Router
的componentWillMount
中,添加了history.listen
,其能够监听路由的变化并执行回调事件,在这里即会触发setState
。当setState
时即每次路由变化时 ->
触发顶层Router
的回调事件 ->
Router
进行setState
->
向下传递 nextContext
此时context
中含有最新的location
->
下面的Route
获取新的nextContext
判断是否进行渲染。
// line packages\react-router\modules\Router.js line 10
class Router extends React.Component {
static computeRootMatch(pathname) {
return { path: "/", url: "/", params: {}, isExact: pathname === "/" };
}
constructor(props) {
super(props);
this.state = {
location: props.history.location
};
// This is a bit of a hack. We have to start listening for location
// changes here in the constructor in case there are any <Redirect>s
// on the initial render. If there are, they will replace/push when
// they mount and since cDM fires in children before parents, we may
// get a new location before the <Router> is mounted.
this._isMounted = false;
this._pendingLocation = null;
if (!props.staticContext) {
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 (
<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
来嵌套Route
,所以此时就到Route
组件,Route
的作用是匹配路由,并传递给要渲染的组件props
,Route
接受上层的Router
传入的context
,Router
中的history
监听着整个页面的路由变化,当页面发生跳转时,history
触发监听事件,Router
向下传递nextContext
,就会更新Route
的props
和context
来判断当前Route
的path
是否匹配location
,如果匹配则渲染,否则不渲染,是否匹配的依据就是computeMatch
这个函数,在下文会有分析,这里只需要知道匹配失败则match
为null
,如果匹配成功则将match
的结果作为props
的一部分,在render
中传递给传进来的要渲染的组件。Route
接受三种类型的render props
,<Route component>
、<Route render>
、<Route children>
,此时要注意的是如果传入的component
是一个内联函数,由于每次的props.component
都是新创建的,所以React
在diff
的时候会认为进来了一个全新的组件,所以会将旧的组件unmount
再re-mount
。这时候就要使用render
,少了一层包裹的component
元素,render
展开后的元素类型每次都是一样的,就不会发生re-mount
了,另外children
也不会发生re-mount
。
// \packages\react-router\modules\Route.js line 17
class Route extends React.Component {
render() {
return (
<RouterContext.Consumer>
{context => {
invariant(context, "You should not use <Route> outside a <Router>");
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;
// Preact uses an empty array as children by
// default, so use null if that's the case.
if (Array.isArray(children) && children.length === 0) {
children = null;
}
if (typeof children === "function") {
children = children(props);
// ...
}
return (
<RouterContext.Provider value={props}>
{children && !isEmptyChildren(children)
? children
: props.match
? component
? React.createElement(component, props)
: render
? render(props)
: null
: null}
</RouterContext.Provider>
);
}}
</RouterContext.Consumer>
);
}
}
我们实际上我们可能写的最多的就是Link
这个标签了,所以我们再来看一下<Link>
组件,我们可以看到Link
最终还是创建一个a
标签来包裹住要跳转的元素,在这个a
标签的handleClick
点击事件中会preventDefault
禁止默认的跳转,所以实际上这里的href
并没有实际的作用,但仍然可以标示出要跳转到的页面的URL
并且有更好的html
语义。在handleClick
中,对没有被preventDefault
、鼠标左键点击的、非_blank
跳转的、没有按住其他功能键的单击进行preventDefault
,然后push
进history
中,这也是前面讲过的路由的变化与 页面的跳转是不互相关联的,ReactRouter
在Link
中通过history
库的push
调用了HTML5 history
的pushState
,但是这仅仅会让路由变化,其他什么都没有改变。在Router
中的listen
,它会监听路由的变化,然后通过context
更新props
和nextContext
让下层的Route
去重新匹配,完成需要渲染部分的更新。
// packages\react-router-dom\modules\Link.js line 14
class Link extends React.Component {
handleClick(event, history) {
if (this.props.onClick) this.props.onClick(event);
if (
!event.defaultPrevented && // onClick prevented default
event.button === 0 && // ignore everything but left clicks
(!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 ? history.replace : history.push;
method(this.props.to);
}
}
render() {
const { innerRef, replace, to, ...rest } = this.props; // eslint-disable-line no-unused-vars
return (
<RouterContext.Consumer>
{context => {
invariant(context, "You should not use <Link> outside a <Router>");
const location =
typeof to === "string"
? createLocation(to, null, null, context.location)
: to;
const href = location ? context.history.createHref(location) : "";
return (
<a
{...rest}
onClick={event => this.handleClick(event, context.history)}
href={href}
ref={innerRef}
/>
);
}}
</RouterContext.Consumer>
);
}
}
每日一题
https://github.com/WindrunnerMax/EveryDay
参考
https://zhuanlan.zhihu.com/p/44548552
https://github.com/fi3ework/blog/issues/21
https://juejin.cn/post/6844903661672333326
https://juejin.cn/post/6844904094772002823
https://juejin.cn/post/6844903878568181768
https://segmentfault.com/a/1190000014294604
https://github.com/youngwind/blog/issues/109
http://react-guide.github.io/react-router-cn/docs/guides/basics/Histories.html
ReactRouter的实现的更多相关文章
- react-router 组件式配置与对象式配置小区别
1. react-router 对象式配置 和 组件式配置 组件式配置(Redirect) ----对应---- 对象式配置(onEnter钩子) IndexRedirect -----对应-- ...
- ES6环境搭建及react-router学习
一.起因 ES6新纳入了很多振奋人心的新特性,真的很让人忍不住去尝试一下.不过,由于现在大部分的浏览器对ES6的支持程度都不是很好.所以如果想要放心地使用一些新特性,还需要用一些工具,将ES6或者ES ...
- 实例讲解react+react-router+redux
前言 总括: 本文采用react+redux+react-router+less+es6+webpack,以实现一个简易备忘录(todolist)为例尽可能全面的讲述使用react全家桶实现一个完整应 ...
- react+react-router+webpack+express+nodejs
react+react-router+webpack+express+nodejs 做SinglePageApplication 支持热加载+ES6 有开发模式和发布模式 https://gith ...
- 基于ReactCSSTransitionGroup实现react-router过渡动画
此前,我使用了react-router库来完成单页应用的路由,从而实现组件之间的切换能力.然而,默认页面的切换是非常生硬的,为了让页面切换更加缓和与舒适,通常的方案就是过渡动画. 这里我调研了2种 ...
- React-Router学习整理
欢迎大家指导与讨论 : ) 一.前言 本文摘要:react-router的基本用法,动画效果与路由,路由权限控制,路由离开确认,根据路由切分的按需加载,路由组件的属性.本文是笔者在学习时整理的笔记,由 ...
- [react-router] hashHistory 和 browserHistory 的区别
react-router提供了三种方式来实现路由,并没有默认的路由,需要在声明路由的时候,显式指定所使用的路由. //v1.x <Router/> //v2.0.0 // hash his ...
- react-router配合webpack实现按需加载
很久没有写博客了.一直感觉没有什么要写的,但是这个东西确实有必要的.使用react开发,不可能一直打包到一个文件.小项目肯定没有问题,但是变大一旦到几兆,这个问题就很严重.现在又Commonjs,AM ...
- react-router+webpack+gulp路由实例
背景:新项目要开始了,有一种想要在新项目中使用react的冲动,应该也是一个单页面的应用,单页应用就涉及到一个路由的问题.于是最近在网上找了蛮多关于react-router的文章,也遇到了许多的坑,经 ...
- 【原】react-router项目实战
摘要: react-router相对于flux和redux来说,比较好容易理解一点和容易入门一点.这个是根据我之前的一个项目,然后我用react+react-router+webpack重新写的. 不 ...
随机推荐
- Spring Data JPA 基础第二篇
主要调用工具类JpaUtils类 package cn.itcast.utils;import javax.persistence.EntityManager;import javax.persist ...
- linux重启后nginx服务无法启动
查看ngin.conf pid的内容 例如: pid /usr/local/nginx/logs/nginx.pid 根据以上配置内容来做,检查/usr/local/nginx/logs/是否存在,如 ...
- 一听就懂:用Python做一个超简单的小游戏
写它会用到 while 循环random 模块if 语句输入输出函数
- Python写一个对象,让它自己能够迭代
仿写range()对象,对象是可迭代的: 1 #!usr/bin/env python3 2 # -*- coding=utf-8 -*- 3 4 class myRange(): 5 #初始化,也叫 ...
- CentOS 8 部署 ASP.NET Core 3.1 应用程序 kestrel+Nginx IIS+kestrel
vs2019发布到IIS 下载文档https://files.cnblogs.com/files/netlock/%E4%BD%BF%E7%94%A8VS2019%E5%8F%91%E5%B8%83 ...
- C#中RDLC报表中日期显示格式
转换为日期类型再格式化 =CDate(Fields!UseDate.Value).ToString("yyyy-MM-dd") 使用Format ==Format(Fields!C ...
- 获取Web项目中的控制器类以及类中Action方法
前言 在使用时需要修改命名空间.需要过滤控制器.需要过滤Action方法.结果生成表的插入语句. 代码 public ActionResult ReloadData() { #region 获取所有的 ...
- 手摸手带你学CSS
好好学习,天天向上 本文已收录至我的Github仓库DayDayUP:github.com/RobodLee/DayDayUP,欢迎Star HTML常用标签总结 前言 在大一的时候,上过网页设计的课 ...
- Java:利用BigDecimal类巧妙处理Double类型精度丢失
目录 本篇要点 经典问题:浮点数精度丢失 十进制整数如何转化为二进制整数? 十进制小数如何转化为二进制数? 如何用BigDecimal解决double精度问题? new BigDecimal(doub ...
- 扫盲:Kotlin 的泛型
引子 相信总是有很多同学,总是在抱怨泛型无论怎么学习,都只是停留在一个简单使用的水平,所以一直为此而备受苦恼. Kotlin 作为一门能和 Java 相互调用的语言,自然也支持泛型,不过 Kotlin ...