在單頁面應用如日中天發展的過程中,備受關注的少了前端路由。
而且還經常會被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的兩個路由的原理。環境的配置,我簡單的貼一下,這裡不講。如果你對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"
複製程式碼
專案目錄
執行
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跨元件通訊,如果你不懂的話,可以看看本渣這遍文章,本渣例舉了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} />
}
複製程式碼