手写React-router
本文章将参照官方的react-router-dom,按照其部分组件最基本的功能,自己写一个react-router。其中包括HashRouter、Route、Switch、Link、Redirect,只实现了一些基本功能,如有错误以及不足之处,请评论留言指出。
1.基本环境搭建
首先,我已经配置好了webpack环境,当然你也可以用脚手架创建一个项目,并安装好官方的react-router-dom,用于稍后的测试使用。我的项目文件目录如下:
其中,index.js为项目入口文件。代码如下:
//index.js
import ReactDOM from 'react-dom'
import React from 'react'
import App from './App'
ReactDOM.render(
<App/>, document.getElementById('root')
)
我在App.js中先写几个普通的组件,分别是Home(主页)、About(关于)、User(用户中心)、UserProfile(用户配置)、NotFound用于稍后的使用。
//App.js
import React from 'react'
function Home() {
return (
<div className="Home">
Home
</div>
)
}
function About() {
return (
<div className="About">
About
</div>
)
}
function User(props) {
return (
<div className="User">
User
</div>
)
}
function UserProfile() {
return (
<div className="UserProfile">
UserProfile
</div>
)
}
function NotFound() {
return (
<div className="NotFound">
NotFound
</div>
)
}
class App extends React.Component {
constructor(props) {
super(props)
}
render() {
return (
<div className="App">
<Home />
<About />
<User />
<UserProfile />
</div>
)
}
}
export default App
使用npm start 启动项目,运行正常。
2. HashRouter和数据传递
2.1 HashRouter组件的创建
Router组件分为BrowserRouter和HashRouter。我们写的是HashRouter。
首先观察官方使用HashRouter的引入方式。
import { HashRouter as Router } from 'react-router-dom'
我们在src目录下新建一个react-router-dom的文件夹,在文件夹中新建一个index.js。这样的话,我们在引入的时候,路径可以直接引入到react-router-dom,从而省略index.js。
然后在react-router-dom文件夹中新建一个HashRouter.js。写一个HashRouter组件,在componentDidMount函数中,初始化一下当前页面的默认hash值。最后将组件导出。
在react-router-dom/index.js中引入HashRouter组件,并再次以对象的形式导出。
代码如下:
//react-router-dom/HashRouter.js
import React, { Component } from 'react'
class HashRouter extends Component {
constructor(props){
super(props)
//初始化数据 获取当前的location
this.state = {
//使用slice是为了去掉 #
location: window.location.pathname.slice(1) || '/'
}
}
componentDidMount() {
//设置默认的hash
window.location.hash = window.location.hash || '/'
}
render() {
return (
<div>
我是HashRouter组件
</div>
)
}
}
export default HashRouter
现在,我们就可以在App.js中使用HashRouter这个组件了。在App.js中引入HashRouter并命名为Router,测试一下。
//App.js
import React from 'react'
import { HashRouter as Router } from './react-router-dom'
function Home() {
...
}
function About() {
...
}
function User(props) {
...
}
function UserProfile() {
...
}
function NotFound() {
...
}
class App extends React.Component {
constructor(props) {
super(props)
}
render() {
return (
<div className="App">
<Router />
</div>
)
}
}
export default App
结果页面中正常显示了HashRouter组件的内容,并携带默认hash值。
注: 下文中将HashRouter组件都称为Router组件。
2.2 Route组件的创建
我们知道Router组件是以双标签的形式存在的,并且将其做为外层组件,里边包含route组件和其他组件等。
我们在react-router-dom文件夹下新建一个Route.js,并导出。并且在react-router-dom的index.js中引入并再次导出,便于其他地方使用。代码如下:
//react-router-dom/Route.js
import React from 'react'
function Route() {
return (
<div>
我是Route组件
</div>
)
}
export default Route
//react-router-dom/index.js
import HashRouter from './HashRouter'
import Route from './Route'
export {
HashRouter,
Route
}
Route组件基本创建完毕以后,我们测试一下。需要将HashRouter组件的内容修改为this.props.children。
//App.js
import React from 'react'
import { HashRouter as Router, Route } from './react-router-dom'
...
...
class App extends React.Component {
constructor(props) {
super(props)
}
render() {
return (
<div className="App">
<Router>
<Route />
</Router>
</div>
)
}
}
export default App
//react-router-dom/HashRouter.js
import React, { Component } from 'react'
class HashRouter extends Component {
constructor(props){
super(props)
//初始化数据 获取当前的location
this.state = {
//使用slice是为了去掉 #
location: window.location.pathname.slice(1) || '/'
}
}
componentDidMount() {
//设置默认的hash
window.location.hash = window.location.hash || '/'
}
render() {
return (
<div>
{this.props.children}
</div>
)
}
}
export default HashRouter
结果页面显示出了Route组件的内容,证明是没有问题的。
2.3 利用Context进行数据传递
首先,使用官方react-router,写一个最基本的路由,渲染一个组件。观察一下该组件中的this.props。
在目录中新建一个App2.js,用于做测试。
import React from 'react'
import { HashRouter as Router, Route } from 'react-router-dom'
function Home(props) {
console.log(props);
return (
<div className="Home">
Home
</div>
)
}
class App2 extends React.Component {
constructor(props) {
super(props)
}
render() {
return (
<div className="App">
<Router>
<Route path="/home" component={Home} />
</Router>
</div>
)
}
}
export default App2
组件里面打印props内容如下:
这里有三个对象,history、location、match,这三个对象都是我们需要关注的。首先,我们将会利用到history的push方法跳转路由。会用到location中的pathname属性,记录当前的路径名。会用到match对象中的isExact精确匹配路由,和params对象,获取路由传递的参数。
由官方的react-router测试,我们可知,通过Router组件包裹Route组件后,会给Route组件渲染的component组件传递以上的数据。
查阅react官方文档,我们找到了React.createContext这个API,用它来传递数据。
在react-router-dom文件夹下新建一个context.js。通过调用React.createContext()方法, 获取Provider(用来提供数据)和Consumer(同来消费数据)。
//react-router-dom/context.js
const myContext = React.createContext()
const { Provider, Consumer } = myContext
export {
Provider,//提供数据
Consumer//消费数据
}
因为HashRouter是最外层组件,包裹了route组件。所以要在Router组件提供数据,在Route组件消费数据。
( 注:此时Router组件就是HashRouter组件。)
我们在Router中使用Provider来提供数据。通过value的形式将数据传递出去。
并且利用onhashchange事件来监听hash值的变化。在这里onhashchange事件要用箭头函数,this才指的是组件实例,才能调用setState方法。
//react-router-dom/HashRouter.js
import React, { Component } from 'react'
import { Provider } from './context'
class HashRouter extends Component {
constructor(props){
super(props)
//初始化数据 获取当前的location
this.state = {
//使用slice是为了去掉 #
location: {
pathname: window.location.hash.slice(1) || '/'
}
}
}
componentDidMount() {
//设置默认的hash
window.location.hash = window.location.hash || '/'
//监听hash值的变化
window.onhashchange = () => {
this.setState({
location: {
pathname: window.location.hash.slice(1)
}
})
}
}
render() {
const value = {
location: this.state.location,
//history这里先放一个空对象
history:{}
}
//Provider提供数据 value就是传递出去的数据
return (<Provider value={value}>{this.props.children}</Provider>)
}
}
export default HashRouter
数据提供出去以后,我们需要在Route组件中来获取消费数据。通过查阅官方文档,我们查到了Consumer的使用方法。
注意: 这里需要函数组件来使用Consumer,类组件会报错。具体的报错解决方法,不是本文章的主要内容,这里没有做具体的深究。
在Route.js中使用Consumer,并在标签中写一对花括号表示我们要写js代码。然后写一个箭头函数用来消费Provider提供的数据,value是形参,也就是Provider传递过来的数据。
//react-router-dom/Route.js
import React from 'react'
import { Consumer } from './context'
function Route() {
return (
<Consumer>
{
//定义一个函数,来消费Provider提供的数据
value => {
console.log('value::', value);
return null
}
}
</Consumer>
)
}
export default Route
此时,我们在Router中用Provider提供数据,数据中记录的是当前页面的hash值。在Route中用Consumer消费数据。并且绑定了一个监听页面hash值变化的事件。
此时我们能做到:
如果地址栏路径后的hash值变化==>
onhashchange事件触发,调用setState ==>
页面刷新,将最新的location对象通过Provider提供出去 ==>
在Route组件中通过Consumer获取数据。
3. 根据正则匹配路由,渲染相应组件
3.1 安装path-to-regexp
npm install path-to-regexp
3.2 Route组件中路由的匹配
Route组件的核心功能:
路由的匹配(注意exact属性的处理),根据匹配结果返回对应的组件。
//App.js
import React from 'react'
import { HashRouter as Router, Route } from './react-router-dom'
function Home() {
return (
<div className="Home">
Home
</div>
)
}
...
...
class App extends React.Component {
constructor(props) {
super(props)
}
render() {
return (
<div className="App">
<Router>
{/* 给Route组件传递path属性和component */}
<Route path="/home" component={Home} />
</Router>
</div>
)
}
}
export default App
在Route组件中,根据props中的path生成正则对象。用Provider提供的pathname去match。
如果match到了,则处理参数。在我们关注的三个对象中,location和history是Provider提供过来的,match是根据每次路径中不同的hash值,渲染不同的组件,临时生成的。
//react-router-dom/Route.js
import React from 'react'
import { pathToRegexp } from 'path-to-regexp'
import { Consumer } from './context'
function Route(props) {
return (
<Consumer>
{
//定义一个函数,来消费Provider提供的数据
value => {
//拿到Provider提供的pathname
const { pathname } = value.location
//拿到组件传递过来的数据
//exact默认为false
//component重新命名为首字母大写
const { path, component: Component, exact = false } = props
//根据path生成正则
const paramNames = []
//end是ture表示在正则结尾添加$,是false就不添加
const reg = pathToRegexp(path, paramNames, { end: exact })
//匹配结果
const result = pathname.match(reg)
if (result) {
//处理参数
const names = paramNames.map(item => item.name)
const [url, ...values] = result
const params = {}
names.forEach((name, index) => {
params[name] = values[index]
})
const props = {
location: value.location,
history: value.history,
match: {
params,
path,
url,
isExact: exact
}
}
//将传递来的组件返回出去,进行渲染
return <Component {...props} />
}
return null
}
}
</Consumer>
)
}
export default Route
4. Link组件的实现
4.1 Link组件
Link组件的本质是返回一个a标签,点击后跳转到指定的地址。这里我们就需要用到history上的push方法。
修改HashRouter.js,在history上添加push方法。
//HashRouter.js
import React, { Component } from 'react'
import { Provider } from './context'
class HashRouter extends Component {
constructor(props){
...
}
componentDidMount() {
...
}
render() {
const value = {
location: this.state.location,
history: {
push(to){
window.location.hash = to
}
}
}
//Provider提供数据 value就是传递出去的数据
return (<Provider value={value}>{this.props.children}</Provider>)
}
}
export default HashRouter
react-router-dom文件夹下新建Link.js。
注意: 在return 的a标签中,href属性如果写’javascript:;'会报一个警告,但是如果写"#",则会改变hash值。所以拼接字符串,写成每次传递来的hash值。
//react-router-dom/Link.js
import React from 'react'
import { Consumer } from './context'
function Link(props) {
return (
<Consumer>
{
//定义一个函数,来消费Provider提供的数据
value => {
//点击调用value.history.push方法
return <a href={`#${props.to}`} onClick={()=>value.history.push(props.to)} >{props.children}</a>
}
}
</Consumer>
)
}
export default Link
在react-router-dom的index.js中引入并导出
//react-router-dom/index.js
//引入相关的组件
import HashRouter from './HashRouter'
import Route from './Route'
import Link from './Link'
//以对象的形式导出
export {
HashRouter,
Route,
Link
}
5. Switch组件的实现
5.1 Switch组件
Switch组件的功能是,匹配到一个路由后,就不再往下匹配了。
我们在Switch组件中遍历所有的Route组件,比对路径,匹配到以后再return。
//react-router-dom/Switch.js
import React from 'react'
import { pathToRegexp } from 'path-to-regexp'
import { Consumer } from './context'
function Switch(props) {
return (
<Consumer>
{
//定义一个函数,来消费Provider提供的数据
value => {
const children = props.children
//获取当前的pathname
const pathname = value.location.pathname
for (let i = 0, len = children.length; i < len; i++) {
let child = children[i]
//获取child的属性
const { path="", exact=false } = child.props
//生成正则
const paramNames = []
const reg = pathToRegexp(path, paramNames, { end: exact })
//匹配结果
const result = pathname.match(reg)
if (result) {
//如果匹配到,将当前child return出去
return child
}
}
//如果都每匹配到,返回null
return null
}
}
</Consumer>
)
}
export default Switch
//react-router-dom/index.js
//引入相关的组件
import HashRouter from './HashRouter'
import Route from './Route'
import Link from './Link'
import Switch from './Switch'
//以对象的形式导出
export {
HashRouter,
Route,
Link,
Switch
}
6. Redirect组件的实现
6.1 Redirect组件
Redirect组件的核心改变路由让页面重写渲染。
//react-router-dom/Redirect.js
import React from 'react'
import { Consumer } from './context'
function Redirect(props) {
return (
<Consumer>
{
//定义一个函数,来消费Provider提供的数据
value => {
value.history.push(props.to)
return null
}
}
</Consumer>
)
}
export default Redirect
//react-router-dom/index.js
//引入相关的组件
import HashRouter from './HashRouter'
import Route from './Route'
import Link from './Link'
import Switch from './Switch'
import Redirect from './Redirect'
//以对象的形式导出
export {
HashRouter,
Route,
Link,
Switch,
Redirect
}