react-router瞭解一下

PlayerWho發表於2018-04-07

清明時節雨紛紛,不如在家擼程式碼。從零開始實現一個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:{} //匹配到的結果
        }
    }
}
複製程式碼
參考

相關文章