在单页面利用如日中天倒退的过程中,备受关注的少了前端路由。
而且还常常会被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} /> }