寫在前面
用react-router v4可以實現單頁面應用,可以將元件對映到路由上,將對應的元件渲染到想要渲染的位置。 react路由有兩種方式:一種是HashRouter,即利用hash實現路由切換。另一種是BrowserRouter,即利用html5 API實現路由的切換。本文是在閱讀react-router v4原始碼之後簡單的實現。
本文將從以下幾部分進行總結:
- 使用react-router v4的一個簡單例子
- 程式碼結構
- Provider和Consumer
- 實現HashRouter
- 實現Route
- 實現Link
- 實現Redirect
- 實現Switch
使用react-router的一個簡單例子
以下是參照react-router 官方文件實現的一個簡單例子:
import React, { Component } from 'react';
import { render } from 'react-dom';
import { HashRouter as Router, Route, Link, Redirect, Switch } from 'react-router-dom';
import Home from './Home';
import Profile from './Profile';
import User from './User';
export default class App extends Component {
constructor() {
super();
}
render() {
return (
<Router>
<div>
<div>
<Link to="/home">首頁</Link>
<Link to="/profile">個人中心</Link>
<Link to="/user">使用者</Link>
</div>
<div>
<Switch>
<Route path="/home" exact={true} component={Home}></Route>
<Route path="/profile" component={Profile}></Route>
<Route path="/user" component={User}></Route>
<Redirect to="/home"></Redirect>
</Switch>
</div>
</div>
</Router>
)
}
}
render(<App></App>, document.querySelector('#root'));
複製程式碼
這樣就能實現一個超級簡單的單頁面應用,根據路徑的變化渲染相應的元件。點選首頁會跳轉到Home元件,點選個人中心會跳轉到Profile元件,點選使用者會跳轉到User元件。在這個例子當中。看下這行程式碼
import { HashRouter as Router, Route, Link, Redirect, Switch } from 'react-router-dom';
複製程式碼
如果不引入'react-router-dom'這個包,而是自己實現一個my-react-router-dom,暴露出HashRouter,Route,Link,Redirect,Switch這幾個元件,並且這幾個元件和react-router-dom提供的功能基本一樣,那究竟怎麼實現呢?
程式碼結構
如上圖所示,在上面的那個例子路由引入'react-router-dom'改為'./my-react-router-dom', 並在同級新建一個my-react-router-dom資料夾,並在index.js中暴露出 HashRouter, Route, Link, Redirect, Switch這幾個方法。// 這是index.js檔案
import HashRouter from './HashRouter';
import Route from './Route';
import Link from './Link';
import Redirect from './Redirect';
import Switch from './Switch';
export {
HashRouter,
Route,
Link,
Redirect,
Switch
}
複製程式碼
Provider和Consumer
再看一下context.js檔案,context可以跨元件傳遞資料:
// 這是context.js檔案
import React, { Component } from 'react';
// 這個方法是16.3新增的
let { Provider, Consumer} = React.createContext();
export { Provider, Consumer};
複製程式碼
舊版context的致命缺陷
‘現有的原生 Context API 存在著一個致命的問題,那就是在 Context 值更新後,頂層元件向目標元件 props 透傳的過程中,如果中間某個元件的 shouldComponentUpdate 函式返回了 false,因為無法再繼續觸發底層元件的 rerender,新的 Context 值將無法到達目標元件。這樣的不確定性對於目標元件來說是完全不可控的,也就是說目標元件無法保證自己每一次都可以接收到更新後的 Context 值。’如何解讀 react 16.3 引入的新 context api ---誠身的回答
新版 Context API 提供Provider和Consumer兩個元件,顧名思義,provider(提供者)和Consumer(消費者):
- Provider 元件:用在元件樹中更外層的位置。它接受一個名為 value 的 prop,其值可以是任何 JavaScript 中的資料型別。
- Consumer 元件:可以在 Provider 元件內部的任何一層使用。它接收一個名為 children 值為一個函式的 prop。這個函式的引數是 Provider 元件接收的那個 value prop 的值,返回值是一個 React 元素(一段 JSX 程式碼)。
實現HashRouter
分析上面的例子,HashRouter的別名設定為Router,傳入的 Router 中的每一項即為一條路由配置,表示在匹配給定的地址時,應該使用什麼元件渲染檢視。HashRouter的簡單實現如下:
// 這是hashRouter.js檔案
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import { Provider } from './context';
export default class HashRouter extends Component {
constructor() {
super();
this.state = {
location: {
// slice(1)將#截掉
pathname: window.location.hash.slice(1) || ''
}
};
}
componentDidMount() {
// 首次進入頁面url會顯示#/
// 如localhost:3000會顯示為localhost:3000/#/
window.location.hash = window.location.hash || '/';
// 監聽hash值變化,重新設定location狀態
window.addEventListener('hashchange', () => {
this.setState({
location: {
...this.state.location,
pathname: window.location.hash.slice(1) || '/'
}
})
})
}
render() {
// 每個子route物件都會包含location
let value = {
location: this.state.location,
history: {
push(to) {
window.location.hash = to;
}
}
}
return (
<Provider value={value}>
{this.props.children}
</Provider>
)
}
}
複製程式碼
這樣一來,使用hashRouter的方式第一次進入頁面時,將顯示#/,如localhost:3000首次進入頁面將會顯示localhost:3000/#/,通過Provider元件來將location和history物件傳遞給子route,通過hashchange方法來監聽hash值的變化,進而重新設定location的狀態。
實現Route
通過分析上面的例子,看這一行程式碼:
<Route path="/home" exact={true} component={Home}></Route>
複製程式碼
發現Route元件包含有path,exact,component這些屬性,那如何實現Route元件呢?看Route.js檔案:
// 這是route.js檔案
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import { Consumer } from './context';
import pathToReg from 'path-to-regexp';
export default class Router extends Component {
constructor() {
super();
}
render() {
return (
<Consumer>
{state => {
// <route path="xx" component="xx" exact={true}></route>
// path是route傳遞的
let { path, component: Component, exact = false } = this.props;
// pathname是location中的
let pathname = state.location.pathname;
// 根據path實現一個正則,通過正則匹配
// location中的/home/123是能匹配到Home元件的
let keys = [];
let reg = pathToReg(path, keys, { end: exact});
keys = keys.map(item => item.name);
let result = pathname.match(reg);
let [url, ...values] = result || [];
// 實現路由跳轉
let props = {
location: state.location,
history: state.history,
match: {
params: keys.reduce((obj, current, index) => {
obj[current] = values[index];
return obj;
}, {})
}
}
if (result) {
return <Component {...props}></Component>
}
return null
}}
</Consumer>
)
}
}
複製程式碼
利用path-to-regexp這個庫來進行是否嚴格匹配路徑,利用新版context的Consumer元件和props來取出path,component,axact這幾個引數。如果匹配到path,則通過<Component {...props}>返回相應匹配到的元件。
實現Link
分析上面的例子,Link的元件實現稍微簡單一些,點選內容跳轉到to屬性對應的路徑即可:
// 這是Link元件
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import { Consumer } from './context';
export default class Link extends Component {
constructor() {
super();
}
render() {
return (
<Consumer>
{state => {
return <a onClick={() => {
state.history.push(this.props.to);
}}>{this.props.children}</a>
}}
</Consumer>
)
}
}
複製程式碼
通過history.push即可實現點選this.props.children的內容跳轉到Link元件的to屬性對應的路徑。
實現Redirect
重定向就是匹配不到後直接跳轉到Redirect中的to路徑:
// 這是Redirect.js檔案
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import { Consumer } from './context';
export default class Redirect extends Component {
constructor() {
super();
}
render() {
return (
<Consumer>
{state => {
// 重定向就是匹配不到後直接跳轉到redirect中的to路徑
state.history.push(this.props.to);
return null;
}}
</Consumer>
)
}
}
複製程式碼
實現Switch
Switch元件的作用就是隻匹配第一個匹配到的元件:
// 這是Switch.js的檔案
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import { Consumer } from './context';
import pathToRegExp from 'path-to-regexp';
// Switch的作用就是匹配一個元件
export default class Switch extends Component {
constructor() {
super();
}
render() {
return (
<Consumer>
{state => {
{
let pathname = state.location.pathname;
// 取出Switch包含的元件
let children = this.props.children;
for ( var i = 0; i < children.length; i++) {
let child = children[i];
// Redirect元件可能沒有path屬性
let path = child.props.path || '';
pathToRegExp(path, [], {end: false});
// switch匹配成功了
if (reg.test(pathname)) {
// 將匹配到的元件返回即可
return child;
}
}
return null;
}
}}
</Consumer>
)
}
}
複製程式碼
通過遍歷this.props.children來進行逐一匹配,如果匹配到相應的路徑,立即返回對應的元件,如果匹配不到,則返回空。
總結
通過上面的例子,發現實現一個簡易版的react-router並不是想象中那麼難。由於時間關係,路由許可權校驗,BrowserRouter,withRoute這些元件並沒有實現。下次有時間再好好分析思考一下如何實現:)
擴充套件閱讀
React 全新的 Context API —— qiqi105
從新的 Context API 看 React 應用設計模式 ——誠身
react-router@4.0 使用和原始碼解析 ——夏爾先生