跳到主要内容

在单页面应用如日中天发展的过程中,备受关注的少了前端路由

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

一大堆为什么,问你头都大,此时,我就要拿出我珍藏的图片了,各位观众,五个烟。🤣

img

前言

今天主要讲的是:

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

四种路由的实现原理。

环境问题

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

img

只可以在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"

项目目录

img

运行

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

img

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。

img

好,现在我们把它壳定好来,让我们来一个一个的弄*它们 😂

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

}
信息

作者:孤猎 链接:https://juejin.cn/post/6844904094772002823 来源:稀土掘金 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。