react后臺管理系統(tǒng)路由方案及react-router原理解析
最近做了一個后臺管理系統(tǒng)主體框架是基于React進行開發(fā)的,因此系統(tǒng)的路由管理,選用了react-router(4.3.1)插件進行路由頁面的管理配置。
實現(xiàn)原理剖析
1、hash的方式
? ? 以 hash 形式(也可以使用 History API 來處理)為例,當 url 的 hash 發(fā)生變化時,觸發(fā) hashchange 注冊的回調,回調中去進行不同的操作,進行不同的內容的展示
function Router() {
this.routes = {};
this.currentUrl = '';
}
Router.prototype.route = function(path, callback) {
this.routes[path] = callback || function(){};
};
Router.prototype.refresh = function() {
this.currentUrl = location.hash.slice(1) || '/';
this.routes[this.currentUrl]();
};
Router.prototype.init = function() {
window.addEventListener('load', this.refresh.bind(this), false);
window.addEventListener('hashchange', this.refresh.bind(this), false);
}
window.Router = new Router();
window.Router.init();
? ? 我們也可以自己進行模擬,可以寫成這樣:
function App() {
// 進入頁面時,先初始化當前 url 對應的組件名
let hash = window.location.hash
let initUI = hash === '#login' ? 'login' : 'register'
let [UI, setUI] = useState(initUI);
let onClickLogin = () => {
setUI('Login')
window.location.hash = 'login'
}
let onClickRegister = () => {
setUI('Register')
window.location.hash = 'register'
}
let showUI = () => {
switch(UI) {
case 'Login':
return <Login/>
case 'Register':
return <Register/>
}
}
return (
<div className="App">
<button onClick={onClickLogin}>Login</button>
<button onClick={onClickRegister}>Register</button>
<div>
{showUI()}
</div>
</div>
);
}
? ? 這樣其實已經滿足我們的要求了,如果我在地址欄里輸入 localhost:8080/#login,就會顯示 。但是這個 “#” 符號不太好看,如果輸入 localhost:8080/login 就完美了。
2、history的方式
? ? H5 提供了一個好用的 history API,使用 window.history.pushState() 使得我們即可以修改 url 也可以不刷新頁面,一舉兩得?,F(xiàn)在只需要修改點擊回調里的 window.location.pathname = ‘xxx’ 就可以了,用 window.history.pushState() 去代替。
function App() {
// 進入頁面時,先初始化當前 url 對應的組件名
let pathname = window.location.pathname
let initUI = pathname === '/login' ? 'login' : 'register'
let [UI, setUI] = useState(initUI);
let onClickLogin = () => {
setUI('Login')
window.history.pushState(null, '', '/login')
}
let onClickRegister = () => {
setUI('Register')
window.history.pushState(null, '', '/register')
}
let showUI = () => {
switch(UI) {
case 'Login':
return <Login/>
case 'Register':
return <Register/>
}
}
return (
<div className="App">
<button onClick={onClickLogin}>Login</button>
<button onClick={onClickRegister}>Register</button>
<div>
{showUI()}
</div>
</div>
);
}
3、link的實現(xiàn)
? ? react-router依賴基礎—history,history是一個獨立的第三方js庫,可以用來兼容在不同瀏覽器、不同環(huán)境下對歷史記錄的管理,擁有統(tǒng)一的API。具體來說里面的history分為三類:
老瀏覽器的history: 主要通過hash來實現(xiàn),對應createHashHistory,通過hash來存儲在不同狀態(tài)下的history信息
高版本瀏覽器: 通過html5里面的history,對應createBrowserHistory,利用HTML5里面的history
node環(huán)境下: 主要存儲在memeory里面,對應createMemoryHistory,在內存中進行歷史記錄的存儲
執(zhí)行URL前進
createBrowserHistory: pushState、replaceState
createHashHistory: location.hash=*** location.replace()
createMemoryHistory: 在內存中進行歷史記錄的存儲
執(zhí)行URL回退
createBrowserHistory: popstate
createHashHistory: hashchange
React組件為什么會更新
? ? 其實無論是react-router. react-redux. 能夠使組件更新的根本原因,還是最后出發(fā)了setState函數;對于react-router,其實是對history原生對象的封裝,重新封裝了push函數,使得我們在push函數執(zhí)行的時候,可以觸發(fā)在Router組件中組件裝載之前,執(zhí)行了history.listener函數,該函數的主要作用就是給listeners數組添加監(jiān)聽函數,每次執(zhí)行history.push的時候,都會執(zhí)行l(wèi)istenrs數組中添加的listener, 這里的listener就是傳入的箭頭函數,功能是執(zhí)行了Router組件的setState函數,Router執(zhí)行了setState之后,會將當前url地址欄對應的url傳遞下去,當Route組件匹配到該地址欄的時候,就會渲染該組件,如果匹配不到,Route組件就返回null;
componentWillMount() {
const { children, history } = this.props
invariant(
children == null || React.Children.count(children) === 1,
'A <Router> may have only one child element'
)
// Do this here so we can setState when a <Redirect> changes the
// location in componentWillMount. This happens e.g. when doing
// server rendering using a <StaticRouter>.
//這里執(zhí)行history.listen()方法;傳入一個函數;箭頭函數的this指的是父級的作用域中的this值;
this.unlisten = history.listen(() => {
this.setState({
match: this.computeMatch(history.location.pathname)
})
})
}
react-router頁面跳轉基本原理
? ? react-router頁面跳轉的時候,主要是通過框架攔截監(jiān)聽location的變化,然后根據location中的pathname去同步相對應的UI組件。
? ? 其中在react-router中,URL對應location對象,而UI是有react components來決定的,因此我們要通過router聲明一份含有path to component的詳細映射關系路由表, 觸發(fā) Link 后最終將通過如上面定義的路由表進行匹配,并拿到對應的 component 及 state 進行 render 渲染頁面。
從點擊 Link 到 render 對應 component ,路由中發(fā)生了什么
? ? Router 在 react component 生命周期之組件被掛載前 componentWillMount 中使用 this.history.listen 去注冊了 url 更新的回調函數。回調函數將在 url 更新時觸發(fā),回調中的 setState 起到 render 了新的 component 的作用。
Router.prototype.componentWillMount = function componentWillMount() {
// .. 省略其他
var createHistory = this.props.history;
this.history = _useRoutes2[‘default‘](createHistory)({
routes: _RouteUtils.createRoutes(routes || children),
parseQueryString: parseQueryString,
stringifyQuery: stringifyQuery
});
this._unlisten = this.history.listen(function (error, state) {
_this.setState(state, _this.props.onUpdate);
});
};
上面的 _useRoutes2 對 history 操作便是對其做一層包裝,所以調用的 this.history 實際為包裝以后的對象,該對象含有 _useRoutes2 中的 listen 方法,如下:
function listen(listener) {
return history.listen(function (location) {
// .. 省略其他
match(location, function (error, redirectLocation, nextState) {
listener(null, nextState);
});
});
}
可看到,上面的代碼中,主要分為兩部分:
使用了 history 模塊的 listen 注冊了一個含有 setState 的回調函數(這樣就能使用 history 模塊中的機制)
回調中的 match 方法為 react-router 所特有,match 函數根據當前 location 以及前面寫的 Route 路由表匹配出對應的路由子集得到新的路由狀態(tài)值 state,具體實現(xiàn)可見 react-router/matchRoutes ,再根據 state 得到對應的 component ,最終執(zhí)行了 match 中的回調 listener(null, nextState) ,即執(zhí)行了 Router 中的監(jiān)聽回調(setState),從而更新了展示。
4、路由懶加載(組件按需加載)
? ? 當React項目過大的時候,如果初次進入將所有的組件文件全部加載,那么將會大大的增加首屏加載的速度,進而影響用戶體驗。因此此時我們需要將路由組件進行按需加載,也就是說,當進入某個URL的時候,再去加載其對應的react component。目前路由的按需加載主要有以下幾種方式:
1)react-loadable
? ?利用react-loadable這個高級組件,要做到實現(xiàn)按需加載這一點,我們將使用的webpack,react-loadable。使用實例如下:
import Loadable from 'react-loadable';
import Loading from './Loading';
const LoadableComponent = Loadable({
loader: () => import('./Dashboard'),
loading: Loading,
})
export default class LoadableDashboard extends React.Component {
render() {
return <LoadableComponent />;
}
}
2)在router3中的按需加載方式
? ?route3中實現(xiàn)按需加載只需要按照下面代碼的方式實現(xiàn)就可以了。在router4以前,我們是使用getComponent的的方式來實現(xiàn)按需加載,getComponent是異步的,只有在路由匹配時才會調用,router4中,getComponent方法已經被移除,所以這種方法在router4中不能使用。
const about = (location, cb) => {
require.ensure([], require => {
cb(null, require('../Component/about').default)
},'about')
}
//配置route
<Route path="helpCenter" getComponent={about} />
3)異步組件
創(chuàng)建一個異步組件 AsyncComponent
import React from 'react';
export default function (getComponent) {
return class AsyncComponent extends React.Component {
static Component = null;
state = { Component: AsyncComponent.Component };
componentWillMount() {
if (!this.state.Component) {
getComponent().then(({default: Component}) => {
AsyncComponent.Component = Component
this.setState({ Component })
})
}
}
render() {
const { Component } = this.state
if (Component) {
return <Component {...this.props} />
}
return null
}
}
}
使用異步組件:我們將使用asyncComponent動態(tài)導入我們想要的組件。
import asyncComponent from './asyncComponent'
const Login = asyncComponent(() => load('login/login'))
const LayoutPage = asyncComponent(() => load('layout/layout'))
const NoticeDeatil = asyncComponent(() => load('noticeDetail/noticeDetail'))
export const appRouterMap = [
{path:"/login",name:"Login",component:Login,auth:false},
{path:"/web",name:"LayoutPage",component:LayoutPage,auth:false},
{path:"/notice/:id",name:"NoticeDeatil",component:NoticeDeatil,auth:false},
]
使用方法
? ?這次主要是做一個后臺管理系統(tǒng),因此使用的時候,需要考慮到頁面的內容區(qū)域以及固定區(qū)域的區(qū)別。內容區(qū)域的內容隨url的變化而變化,單固定區(qū)域內容保持不變,因此常規(guī)的路由配置并不能滿足該需求。因此使用的Route嵌套Route的方式實現(xiàn),外層Route控制固定區(qū)域的變化,內層Route控制內容區(qū)域的變化。使用實現(xiàn)步驟如下:
1、安裝相關依賴
npm install react-router react-router-dom -S
2、配置路由—URL關系映射表
固定區(qū)域路由配置
import login from '../pages/login/login'
import home from '../pages/home/home'
// `/`和`:`為關鍵字,不能作為參數傳遞
let routers = [{
name: 'login',
path: '/login',
title: '登錄',
exact: true,
component: login
}, {
name: 'home', // 名稱,必須唯一
path: '/home', // 路徑,第一個必須為'/',主名字必須唯一,瀏覽器導航路徑(url)
title: '主頁', // 頁面title及導航欄顯示的名稱
exact: false, // 嚴格匹配
component: home
}
]
export default routers
內容區(qū)域路由配置(此處使用了上面的第一種路由懶加載方法)
import React from 'react'
import Loadable from "react-loadable"
// 注意:參數名不能和路由任何一個path名相同;除path的參數后,path和name必須一樣;`/`和`:`為關鍵字,不能作為參數傳遞,parent: 'testpage', // 如果是二級路由,需要指定它的父級(必須)
let routers = [
{
name: 'testpage',
path: '/system/user',
title: '用戶管理',
exact: false,
component: Loadable({
loader: () => import('../pages/system/user/user'),
loading: () => <div className="page-loading"><span>加載中......</span></div>
})
},
]
export default routers
3、將路由注入項目
固定區(qū)域(關鍵代碼如下)
import Loading from '@components/Loading'
import routers from './routers'
import { HashRouter, Route, Switch, Redirect } from 'react-router-dom'
import page404 from './pages/404/404'
const App = () => {
return (
<div className="app">
<Loading />
<Switch>
{routers.map((r, key) => (
<Route key={key}
{...r} />
))}
<Redirect from="/"
to={'/login'}
exact={true} />
<Route component={page404} />
</Switch>
</div>
)
}
ReactDOM.render(
<HashRouter>
<ConfigProvider locale={zhCN}>
<App />
</ConfigProvider>
</HashRouter>,
document.getElementById('root')
)
內容區(qū)域(home文件關鍵代碼如下)
import { Redirect, Route, Switch } from "react-router-dom"
import routers from '../../views/router'
import Page404 from '../404/404'
....省略無數代碼
<Content className={styles.content}>
<Switch>
{routers && routers.map((r, key) => {
const Component = r.component,
return <Route key={key}
render={props => <Component {...props}
allRouters={routers}
/>}
exact={r.exact}
path={match + r.path} />
})}
<Route component={Page404} />
</Switch>
</Content>
....省略無數代碼
歡迎關注微信公眾號:猴哥說前端