清明時節雨紛紛,不如在家擼程式碼。從零開始實現一個react-router,並跑通react-router-dom裡的example。
react-router是做SPA(不是你想的SPA)時,控制不同的url渲染不同的元件的js庫。用react-router可以方便開發,不需要手動維護url和元件的對應關係。開發時用react-router-dom,react-router-dom裡面的元件是對react-router元件的封裝。
SPA的原理
單頁應用的原理用兩種,一種是通過hash的變化,改變頁面,另一種是通過url的變化改變頁面。
- hash
- window.location.hash='xxx' 改變hash
- window.addEventListener('hashchange',fun) 監聽hash的改變
- url
- history.pushState(obj,title,'/url') 改變url
- window.addEventListener('popstate',fun) 當瀏覽器向前向後時,觸發該事件。
React-Router-dom的核心元件
- Router
- Router是一個外層,最後render的是它的子元件,不渲染具體業務元件。
- 分為HashRouter(通過改變hash)、BrowserRouter(通過改變url)、MemoryRouter
- Router負責選取哪種方式作為單頁應用的方案hash或browser或其他的,把HashRouter換成BrowserRouter,程式碼可以繼續執行。
- Router的props中有一個history的物件,history是對window.history的封裝,history的負責管理與瀏覽器歷史記錄的互動和哪種方式的單頁應用。history會作為childContext裡的一個屬性傳下去。
- Route
- 負責渲染具體的業務元件,負責匹配url和對應的元件
- 有三種渲染的元件的方式:component(對應的元件)、render(是一個函式,函式裡渲染元件)、children(無論哪種路由都會渲染)
- Switch
- 匹配到一個Route子元件就返回不再繼續匹配其他元件。
- Link
- 跳轉路由時的元件,呼叫history.push把改變url。
history
- history是管理與瀏覽器歷史記錄的互動,和用哪種方式實現單頁應用。這裡實現了一個簡單的history
- MyHistory是父類,HashHistory、BrowserHistory是兩個子類。
- HashHistory和BrowserHistory例項的loaction屬性是相同的,所以updateLocation是子類方法。location的pathname在HashHistory是hash的#後面的值,在BrowserHistory是window.location.pathname。
- 兩個子類裡有一個_push方法,用來改變url,都是用的history.pushState方法。
let confirm;
export default class MyHistory {
constructor() {
this.updateLocation();//改變例項上的location變數,子類實現
}
go() {
//跳到第幾頁
}
goBack() {
//返回
}
goForward() {
//向前跳
}
push() {
//觸發url改變
if (this.prompt(...arguments)) {
this._push(...arguments);//由子類實現
this.updateLocation();
this._listen();
confirm = null; //頁面跳轉後把confirm清空
}
}
listen(fun) {
//url改變後監聽函式
this._listen = fun;
}
createHref(path) {
// Link元件裡的a標籤的href
if (typeof path === 'string') return path;
return path.pathname;
}
block(message) {
//window.confirm的內容可能是傳入的字串,可能是傳入的函式返回的字串
confirm = message;
}
prompt(pathname) {
//實現window.confirm,確定後跳轉,否則不跳轉
if (!confirm) return true;
const location = Object.assign(this.location,{pathname});
const result = typeof confirm === 'function' ? confirm(location) : confirm;
return window.confirm(result);
}
}
複製程式碼
import MyHistory from './MyHistory';
class HashHistory extends MyHistory {
_push(hash) {
//改變hash
history.pushState({},'','/#'+hash);
}
updateLocation() {
//獲取location
this.location = {
pathname: window.location.hash.slice(1) || '/',
search: window.location.search
}
}
}
export default function createHashHistory() {
//建立HashHistory
const history = new HashHistory();
//監聽前進後退事件
window.addEventListener('popstate', () => {
history.updateLocation();
history._listen();
});
return history;
};
複製程式碼
import MyHistory from './MyHistory';
class BrowserHistory extends MyHistory{
_push(path){
//改變url
history.pushState({},'',path);
}
updateLocation(){
this.location = {
pathname:window.location.pathname,
search:window.location.search
};
}
}
export default function createHashHistory(){
//建立BrowserHistory
const history = new BrowserHistory();
window.addEventListener('popstate',()=>{
history.updateLocation();
history._listen();
});
return history;
};
複製程式碼
Router
- HashRouter和BrowserRouter是對Router的封裝,傳入Router的history物件不同
- Router中要建立childContext,history是props的history,location是history裡的location,match是Route元件裡匹配url後的結果
- history的listen傳入函式,url改變後重新渲染
import PropTypes from 'prop-types';//型別檢查
export default class HashRouter extends Component {
static propTypes = {
history: PropTypes.object.isRequired,
children: PropTypes.node
}
static childContextTypes = {
history: PropTypes.object,
location: PropTypes.object,
match:PropTypes.object
}
getChildContext() {
return {
history: this.props.history,
location: this.props.history.location,
match:{
path: '/',
url: '/',
params: {}
}
}
}
componentDidMount() {
this.props.history.listen(() => {
this.setState({})
});
}
render() {
return this.props.children;
}
}
複製程式碼
import React,{Component} from 'react';
import PropTypes from 'prop-types';
import {createHashHistory as createHistory} from './libs/history';
import Router from './Router';
export default class HashRouter extends Component{
static propTypes = {
children:PropTypes.node
}
history = createHistory()
render(){
return <Router history={this.history} children={this.props.children}/>;
}
}
複製程式碼
import {createBrowserHistory as createHistory} from './libs/history';
export default class BrowserRouter extends Component{
static propTypes = {
children:PropTypes.node
}
history = createHistory()
render(){
return <Router history={this.history} children={this.props.children}/>;
}
}
複製程式碼
Route
- Route通過props裡的path和url進行匹配,匹配到了,渲染元件,繼續匹配下一個。
- Route裡用到path-to-regexp匹配路徑,獲取匹配到的params
- Route有三種渲染元件的方法,要分別處理
- Route的props有一個exact屬性。如果是true,匹配時到path結束,要和location.pathname準確匹配。
- Route的Context是Router建立的location、history、match,每次匹配完成,要改變Route裡的match。
import pathToRegexp from 'path-to-regexp';
export default class Route extends Component {
static contextTypes = {
location: PropTypes.object,
history: PropTypes.object,
match:PropTypes.object
}
static propTypes = {
component: PropTypes.func,
render: PropTypes.func,
children: PropTypes.func,
path: PropTypes.string,
exact: PropTypes.bool
}
static childContextTypes = {
history:PropTypes.object
}
getChildContext(){
return {
history:this.context.history
}
}
computeMatched() {
const {path, exact = false} = this.props;
if(!path) return this.context.match;
const {location: {pathname}} = this.context;
const keys = [];
const reg = pathToRegexp(path, keys, {end: exact});
const result = pathname.match(reg);
if (result) {
return {
path: path,
url: result[0],
params: keys.reduce((memo, key, index) => {
memo[key.name] = result[index + 1];
return memo
}, {})
};
}
return false;
}
render() {
let props = {
location: this.context.location,
history: this.context.history
};
const { component: Component, render,children} = this.props;
const match = this.computeMatched();
if(match){
props.match = match;
if (Component) return <Component {...props} />;
if (render) return render(props);
}
if(children) return children(props);
return null;
}
}
複製程式碼
Switch
- Switch可以套在Route的外面,匹配到了一個Route,就不再往下匹配。
- Switch也用到了路徑匹配,和Route裡的方法類似,可以提取出來,在react-router-dom裡的example用到Switch的地方不多,所以沒有提取。
import pathToRegexp from 'path-to-regexp';
export default class Switch extends Component{
static contextTypes = {
location:PropTypes.object
}
constructor(props){
super(props);
this.path = props.path;
this.keys = [];
}
match(pathname,path,exact){
return pathToRegexp(path,[],{end:exact}).test(pathname);
}
render(){
const {location:{pathname}} = this.context;
const children = this.props.children;
for(let i = 0,l=children.length;i<l;i++){
const child = children[i];
const {path,exact} = child.props;
if(this.match(pathname,path,exact)){
return child
}
}
return null;
}
}
複製程式碼
Link
- Link的屬性to是字串或物件。
- Link渲染a標籤,在a標籤上繫結事件,進行跳轉。
- Link的跳轉是用history.push完成的,a的href屬性是history.createHref的返回值。
export default class Link extends Component{
static propsTypes = {
to: PropTypes.oneOfType([PropTypes.string, PropTypes.object]).isRequired
}
static contextTypes = {
history:PropTypes.object
}
onClickHandle=(e)=>{
e.preventDefault();
this.context.history.push(this.href);
}
render(){
const {to} = this.props;
this.href = this.context.history.createHref(to);
return (
<a onClick={this.onClickHandle} href={this.href}>{this.props.children}</a>
);
}
}
複製程式碼
Redirect
- Redirect跳轉到某個路由,不渲染元件
- 通過history.createHref獲得path,history.push跳轉過去
export default class Redirect extends Component{
static propTypes = {
to: PropTypes.oneOfType([PropTypes.string, PropTypes.object]).isRequired
}
static contextTypes = {
history: PropTypes.object
}
componentDidMount(){
const href = this.context.history.createHref(this.props.to);
this.context.history.push(href);
}
render(){
return null;
}
};
複製程式碼
Prompt
- 相當於window.confirm,點選確定後跳轉到想要的連結,點選取消不做操作
- Prompt的屬性when為true是才觸發confirm
export default class Prompt extends Component {
static propTypes = {
when: PropTypes.bool,
message: PropTypes.oneOfType([PropTypes.string, PropTypes.func]).isRequired
}
static contextTypes = {
history: PropTypes.object
}
componentWillMount() {
this.prompt();
}
prompt() {
const {when,message} = this.props;
if (when){
this.context.history.block(message);
}else {
this.context.history.block(null);
}
}
componentWillReceiveProps(nextProps) {
this.prompt();
}
render() {
return null;
}
};
複製程式碼
withRouter
- withRouter實際是一個高階元件,即一個函式返回一個元件。返回的元件外層是Route,Route的children屬性裡渲染接收到的元件。
import React from 'react';
import Route from './Route';
const withRouter = Component => {
const C = (props)=>{
return (
<Route children={props=>{
return (
<Component {...props} />
)
}}/>
)
};
return C;
};
export default withRouter
複製程式碼
總結
react-router、react-router-dom的api還有很多,像Redirect和withRouter還有的許多api。本文的元件只能跑通react-router-dom裡的example。原始碼要複雜的多,通過學習原始碼,並自己實現相應的功能,可以對react及react-router有更深的理解,學到許多程式設計思想,資料結構很重要,像原始碼中Router裡的ChildContext的資料解構,子元件多次用到裡面的方法或屬性,方便複用。
//ChildContext的資料解構
{
router:{
history, //某種 history
route:{
location:history.location,
match:{} //匹配到的結果
}
}
}
複製程式碼