乐趣区

关于前端:经常被问到的reactrouter实现原理详解

在单页面利用如日中天倒退的过程中,备受关注的少了 前端路由

而且还常常会被 xxx 面试官问到,什么是前端路由,它的原理的是什么,它是怎么实现,跳转不刷新页面的 …

一大堆为什么,问你头都大,

前言

明天次要讲的是:

  • 原生 js 实现 hashRouter
  • 原生 js 实现 historyRouter
  • react-router-dom 的 BrowserRouter
  • react-router-dom 的 HistoryRouter

四种路由的实现原理。

环境问题

因为等一下要用到 h5 新增的 pushState() 办法,因为这玩(diao) 意(mao)太矫情了,不反对在本地的 file 协定 运行,不然就会报以下谬误

只能够在 http(s) 协定 运行,这个坑本渣也是踩了很久,踩狐疑本人的性别。

既然用 file 协定 不行那就只能用webpack 搭个简陋坏境了,你也能够用阿帕奇,tomcat… 啊狗啊猫之类的货色代理。

本渣用的是 webpack 环境,也不便等下解说 react-router-dom 的两个路由的原理。环境的配置,我简略的贴一下,这里不讲。

npm i webpack webpack-cli babel-loader @babel-core @babel/preset-env html-webpack-plugin webpack-dev-server -D

webpack.config.js

const path = require('path')

const HtmlWebpackPlugin = require('html-webpack-plugin')

module.exports = {entry:path.resolve(__dirname,'./index.js'),

    output:{filename:'[name].[hash:6].js',

        path:path.resolve(__dirname,'../dist')

    },

    module:{

        rules:[
            {
                test:/\.js$/,

                exclude:/node_module/,

                use:[
                    {
                        loader:'babel-loader',
                        options:{presets:['@babel/preset-env']
                        }
                    }
                ]
            }
        ]

    },

    plugins:[
        new HtmlWebpackPlugin({template:path.resolve(__dirname,'./public/index.html'),
            filename:'index.html'
        })
    ]

}

package.json 的 script 增加一条命令

    "dev":"webpack-dev-server --config ./src/webpack.config.js --open"

我的项目目录

运行

npm run dev

当初所有货色都筹备好了,咱们能够进入主题了。

原生 js 实现 hashRouter

html

<ul>
    <li><a href='#/home'>home</a></li>
    <li><a href='#/about'>about</a></li>
    <div id="routeView"></div>
</ul>

js

window.addEventListener('DOMContentLoaded', onLoad)

window.addEventListener('hashchange', changeView)

let routeView = ''

function onLoad() {routeView = document.getElementById('routeView')

    changeView()}

function changeView() {switch (location.hash) {
        case '#/home':
            routeView.innerHTML = 'home'
            break;
        case '#/about':
            routeView.innerHTML = 'about'
            break;
    }

}

原生 js 实现 hashRouter 次要是监听它的 hashchange 事件的变动,而后拿到对应的 location.hash 更新对应的视图

原生 js 实现 historyRouter

html

<ul>
    <li><a href='/home'>home</a></li>
    <li><a href='/about'>about</a></li>
    <div id="routeView"></div>
</ul>

historyRoute

window.addEventListener('DOMContentLoaded', onLoad)

window.addEventListener('popstate', changeView)

let routeView = ''

function onLoad() {routeView = document.getElementById('routeView')

    changeView()

    let event = document.getElementsByTagName('ul')[0]

    event.addEventListener('click', (e) => {if(e.target.nodeName === 'A'){e.preventDefault()

            history.pushState(null, "", e.target.getAttribute('href'))

            changeView()}

    })
}

function changeView() {switch (location.pathname) {
        case '/home':
            routeView.innerHTML = 'home'
            break;
        case '/about':
            routeView.innerHTML = 'about'
            break;
    }

}

可能实现 history 路由跳转不刷新页面得益与 H5 提供的 pushState(),replaceState()等办法,这些办法都是也能够扭转路由状态(门路),但不作页面跳转,咱们能够通过 location.pathname 来显示对应的视图

react-router-dom

react-router-dom 是 react 的路由,它帮忙咱们在我的项目中实现单页面利用,它提供给咱们两种路由一种基于 hash 段实现的HashRouter,一种基于 H5Api 实现的BrowserRouter

上面咱们来简略用一下。

如果你在用本渣以上提供给你的环境,还要配置一下,上面👇这些货色,如果不是,请疏忽。

npm i react react-dom react-router-dom @babel/preset-react -D

webpack.config.js, 在 js 的 options 配置加一个 preset

index.js 改成这样。

import React from 'react'

import ReactDom from 'react-dom'

import {BrowserRouter, Route, Link} from 'react-router-dom'

function App() {

    return (

        <BrowserRouter>
            <Link to='/home'>home</Link>
            <Link to='/about'>about</Link>
            <Route path='/home' render={()=><div>home</div>}></Route>
            <Route path='/about' render={()=><div>about</div>}></Route>
        </BrowserRouter>

    )
}

ReactDom.render(<App></App>,document.getElementById('root'))

public/index.html

<div id="root"></div>

平时我么只晓得去应用它们,但却很少去思考它是怎么做到,所以导致咱们一被问到,就会懵逼;今日如果你看完这篇文章,本渣 promiss 你不再只会用 react-router,不再是它骑在你身上,而是你能够对它随心所欲。参考 前端进阶面试题具体解答

react-router-dom 的 BrowserRouter 实现

首先咱们在 index.js 新建一个 BrowserRouter.js 文件,咱们来实现本人 BrowserRouter。既然要实现 BrowserRouter,那这个文件就得有三个组件 BrowserRouter,Route,Link。

好,当初咱们把它壳定好来,让咱们来一个一个的弄 * 它们😂

BrowserRouter 组件

BrowserRouter 组件次要做的是将以后的门路往下传,并监听 popstate 事件, 所以咱们要用 Consumer, Provider 跨组件通信,

const {Consumer, Provider} = React.createContext()

export class BrowserRouter extends React.Component {constructor(props) {super(props)
        this.state = {currentPath: this.getParams.bind(this)(window.location.pathname)
        }
    }


    onChangeView() {const currentPath = this.getParams.bind(this)(window.location.pathname)
        this.setState({currentPath});
    };

    getParams(url) {return url}


    componentDidMount() {window.addEventListener("popstate", this.onChangeView.bind(this));
    }

    componentWillUnmount() {window.removeEventListener("popstate", this.onChangeView.bind(this));
    }

    render() {
        return (<Provider value={{ currentPath: this.state.currentPath, onChangeView: this.onChangeView.bind(this) }}>
                 <div>
                    {React.Children.map(this.props.children, function (child) {return child})
                    }
                </div>
            </Provider>
        );
    }
}

Rouer 组件的实现

Router 组件次要做的是通过 BrowserRouter 传过来的以后值,与 Route 通过 props 传进来的 path 比照,而后决定是否执行 props 传进来的 render 函数,

export class Route extends React.Component {constructor(props) {super(props)
    }

    render() {let { path, render} = this.props
        return (
            <Consumer>
                {({currentPath}) => currentPath === path && render()}
            </Consumer>
        )
    }
}

Link 组件的实现

Link 组件次要做的是,拿到 prop, 传进来的 to, 通过 PushState()扭转路由状态,而后拿到 BrowserRouter 传过来的 onChangeView 手动刷新视图

export class Link extends React.Component {constructor(props){super(props)
    }

    render() {let { to, ...props} = this.props
        return (
            <Consumer>
                {({onChangeView}) => (
                    <a
                        {...props}
                        onClick={e => {e.preventDefault();
                            window.history.pushState(null, "", to);
                            onChangeView();}}
                    />
                )}
            </Consumer>
        )

    }

}

残缺代码

import React from 'react'

const {Consumer, Provider} = React.createContext()

export class BrowserRouter extends React.Component {constructor(props) {super(props)
        this.state = {currentPath: this.getParams.bind(this)(window.location.pathname)
        }
    }


    onChangeView() {const currentPath = this.getParams.bind(this)(window.location.pathname)
        this.setState({currentPath});
    };

    getParams(url) {return url}


    componentDidMount() {window.addEventListener("popstate", this.onChangeView.bind(this));
    }

    componentWillUnmount() {window.removeEventListener("popstate", this.onChangeView.bind(this));
    }

    render() {
        return (<Provider value={{ currentPath: this.state.currentPath, onChangeView: this.onChangeView.bind(this) }}>
                <div>
                    {React.Children.map(this.props.children, function (child) {return child})
                    }
                </div>
            </Provider>
        );
    }
}

export class Route extends React.Component {constructor(props) {super(props)
    }

    render() {let { path, render} = this.props
        return (
            <Consumer>
                {({currentPath}) => currentPath === path && render()}
            </Consumer>
        )
    }
}

export class Link extends React.Component {constructor(props){super(props)
    }

    render() {let { to, ...props} = this.props
        return (
            <Consumer>
                {({onChangeView}) => (
                    <a
                        {...props}
                        onClick={e => {e.preventDefault();
                            window.history.pushState(null, "", to);
                            onChangeView();}}
                    />
                )}
            </Consumer>
        )

    }

}

应用

把方才在 index.js 应用的 react-router-dom 换成这个文件门路就 OK。

react-router-dom 的 hashRouter 的实现

hashRouter 就不一个一个组件的说了,跟 BrowserRouter 大同小异, 间接贴残缺代码了

import React from 'react'

let {Provider, Consumer} = React.createContext()

export class HashRouter extends React.Component {constructor(props) {super(props)
        this.state = {currentPath: this.getCurrentPath.bind(this)(window.location.href)
        }
    }

    componentDidMount() {window.addEventListener('hashchange', this.onChangeView.bind(this))
    }

    componentWillUnmount() {window.removeEventListener('hashchange')
    }

    onChangeView(e) {let currentPath = this.getCurrentPath.bind(this)(window.location.href)
        this.setState({currentPath})
    }

    getCurrentPath(url) {let hashRoute = url.split('#')

        return hashRoute[1]
    }

    render() {

        return (<Provider value={{ currentPath: this.state.currentPath}}>
                <div>
                    {React.Children.map(this.props.children, function (child) {return child})
                    }
                </div>
            </Provider>

        )

    }


}

export class Route extends React.Component {constructor(props) {super(props)
    }

    render() {let { path, render} = this.props

        return (
            <Consumer>
                {(value) => {console.log(value)
                        return (value.currentPath === path && render()
                        )
                    }
                }
            </Consumer>
        )

    }

}

export class Link extends React.Component {constructor(props) {super(props)
    }

    render() {let { to, ...props} = this.props

        return <a href={'#' + to} {...props} />

    }
退出移动版