你女友都懂前端路由和react-router實現原理了,你還不懂。

時樾1998發表於2020-03-17

在單頁面應用如日中天發展的過程中,備受關注的少了前端路由

而且還經常會被xxx面試官問到,什麼是前端路由,它的原理的是什麼,它是怎麼實現,跳轉不重新整理頁面的...

一大堆為什麼,問你頭都大,此時,我就要拿出我珍藏的圖片了,各位觀眾,五個煙。?

你女友都懂前端路由和react-router實現原理了,你還不懂。

前言

今天主要講的是:

  • 原生js實現hashRouter
  • 原生js實現historyRouter
  • react-router-dom的BrowserRouter
  • react-router-dom的HistoryRouter

四種路由的實現原理。

環境問題

因為等一下要用到h5新增的pushState() 方法,因為這玩(diao)意(mao)太矯情了,不支援在本地的file協議執行,不然就會報以下錯誤

你女友都懂前端路由和react-router實現原理了,你還不懂。

只可以在http(s)協議 執行,這個坑本渣也是踩了很久,踩懷疑自己的性別。

既然用file協議 不行那就只能用webpack搭個簡陋壞境了,你也可以用阿帕奇,tomcat...啊狗啊貓之類的東西代理。

本渣用的是webpack環境,也方便等下講解react-router-dom的兩個路由的原理。環境的配置,我簡單的貼一下,這裡不講。如果你對webpack有興趣,可以看看本渣的這篇文章,寫得雖然不是很好,但足夠入門。

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"
複製程式碼

專案目錄

你女友都懂前端路由和react-router實現原理了,你還不懂。

執行

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

你女友都懂前端路由和react-router實現原理了,你還不懂。

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。

你女友都懂前端路由和react-router實現原理了,你還不懂。

好,現在我們把它殼定好來,讓我們來一個一個的弄*它們?

BrowserRouter元件

BrowserRouter元件主要做的是將當前的路徑往下傳,並監聽popstate事件,所以我們要用Consumer, Provider跨元件通訊,如果你不懂的話,可以看看本渣這遍文章,本渣例舉了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>
        );
    }
}

複製程式碼

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} />

    }

複製程式碼

相關文章