React進階篇1

小圖子發表於2019-02-28

React路由

  • 不同的路徑渲染不同的元件
  • 有兩種實現方式
    • HashRouter:利用hash實現路由切換
    • BrowserRouter:實現h5 Api實現路由的切換

1.1 hash

<a href="#/a">去a</a>
    <a href="#/b">去b</a>
    <script>
      window.addEventListener('hashchange',()=>{
          console.log(window.location.hash);
      });
    </script>

複製程式碼

1.2 history

  • history物件提供了操作瀏覽器會話歷史的介面。
  • history物件持續追蹤著一組location
  • 除了一組location外,history也儲存一個索引值,用來指向當前所對應的location
  • history
  • History
  • onpopstate

2.跑通路由

2.1 index.js

import React from 'react';
import ReactDOM from 'react-dom';
import {HashRouter as Router,Route} from 'react-router-dom';
import Home from './components/Home';
import User from './components/User';
import Profile from './components/Profile';
ReactDOM.render(
    <Router>
        <div>
          <Route path="/" component={Home} />
          <Route path="/user" component={User} />
          <Route path="/profile" component={Profile}/>
        </div>
    </Router>
,document.getElementById('root'));
複製程式碼

2.2 Home.js

components/Home.js

import React,{Component} from 'react';
export default class Home extends Component{
    render() {
        return (
            <div>Home</div>
        )
    }
}
複製程式碼

2.3 Profile.js

components/Profile.js

import React,{Component} from 'react';
export default class Profile extends Component{
    render() {
        return (
            <div>Profile</div>
        )
    }
}
複製程式碼

2.4 User.js components/User.js

import React,{Component} from 'react';
export default class User extends Component{
    render() {
        return (
            <div>User</div>
        )
    }
}
複製程式碼

2.5 this.props http://localhost:3000/#/user?name=zyfx#top

{
    "match": {
        "path": "/user/:id",   //匹配路徑
        "url": "/user/1",      //位址列中的url
        "isExact": true,       //是否精確匹配
        "params": {"id": "1"}  // 路徑引數物件
    },
    "location": {
        "pathname": "/user/1",  //路徑名
        "search": "?name=zyfx", //查詢字串
        "hash": "#top"          //hash值
        "state":undefined
    },
    "history": {
        "length": 6,            //歷史長度
        "action": "POP",        //動作
        "location": {           //當前應用所處的位置
            "pathname": "/user/1",
            "search": "?name=zyfx",
            "hash": "#top",
            "state":undefined  //location可以擁有與之相關的狀態。這是一些固定的資料,並且不存在於URL之中
        },
        "go":f go(n),//是一個強大的方法,幷包含了goForward與goBack的功能。傳入負數則退後,傳入正數則向前
        "goBack":f goBack(),//返回一層頁面。實際上是將history的索引值減1
        "goForward":f goForward(),//與goBack相對。向前一層頁面
        "listen":f listen(listener),//採用觀察者模式,在location改變時,history會發出通知
        "push":f push(path,state),//方法使能你跳轉到新的location
        "replace":f replace(path,state)//replace方法與push相似,但它並非新增location,而是替換當前索引上的位置,重定向時要使用replace,
        "createHref":f createHref(location)
    }
}
複製程式碼

2.6 createHref

const location = {
  pathname: '/user',
  search: '?id=1',
  hash: '#bottom'
}
const url = history.createHref(location)
const link = document.createElement('a')
a.href = url
// <a href='/user?id=1#bottom'></a>
複製程式碼

react-route

3.1 index.js

src/index.js

import React from 'react';
import ReactDOM from 'react-dom';
import {HashRouter as Router,Route} from './react-router-dom';
import Home from './components/Home';
import User from './components/User';
import Profile from './components/Profile';
ReactDOM.render(
    <Router>
        <div>
          <Route path="/" component={Home} />
            <Route path="/user" component={User} />
          <Route path="/profile" component={Profile}/>
        </div>
    </Router>
,document.getElementById('root'));
複製程式碼

3.2 index.js

src/react-router-dom/index.js

import HashRouter from './HashRouter';
import Route from './Route';

export {
    HashRouter,
    Route
}
複製程式碼

3.3 context.js

src/react-router-dom/context.js

import React from 'react';
// React16.3
let {Provider,Consumer}=React.createContext();
export {Provider,Consumer};
複製程式碼

3.4 HashRouter.js

src/react-router-dom/HashRouter.js

import React,{Component} from 'react';
import {Provider} from './context';
export default class HashRouter extends Component{
    state={
        location: {
            pathname:window.location.hash?window.location.hash.slice(1):'/'
        }
    }
    componentDidMount() {
        //預設hash沒有的時候跳轉到/
        window.location.hash=window.location.hash||'/';
        //監聽hash值變化
        window.addEventListener('hashchange',() => {
            this.setState({
                location: {
                ...this.state.location,
                pathname:window.location.hash?window.location.hash.slice(1):'/'
            }});
        });
    }
    render() {
        let value={
            location:this.state.location
        }
        return (
            <Provider value={value}>
                {this.props.children}
            </Provider>
        )
    }
}
複製程式碼

3.5 Route.js

src/react-router-dom/Route.js

import React,{Component} from 'react';
import {Consumer} from './context';
export default class Route extends Component{
    render() {
        let {path,component: Component}=this.props;
        return (
            <Consumer>
                {
                    value => {
                        let {pathname}=value.location;
                        if (path == pathname) {
                            return <Component/>
                        } else {
                            return null;
                        }
                    }
                }
            </Consumer>
        )
    }
}
複製程式碼

4. path-to-regexp

把一個路徑轉換成正規表示式

let pathToRegexp=require('path-to-regexp');
//let regex=pathToRegexp('/user',[],{end: true});// /^\/user(?:\/)?$/i
//let regex=pathToRegexp('/user',[],{end: false});// //^\/user(?:\/(?=$))?(?=\/|$)/i
let keys=[];
let regex=pathToRegexp('/user/:id',keys,{end: false});///^\/user\/([^\/]+?)(?:\/(?=$))?(?=\/|$)/i
// (?:) 是 不想被捕獲的時候使用
// (?=pattern) 零寬正向先行斷言(zero-width positive lookahead assertion) 
console.log(regex);
console.log(keys);
//console.log(regex.test('/user'));   //true
//console.log(regex.test('/user/1')); //true
console.log(regex.test('/user/1')); //true
複製程式碼

5. 正則匹配路徑

5.1 Route.js

src/react-router-dom/Route.js

import React,{Component} from 'react';
import {Consumer} from './context';
import pathToRegexp from 'path-to-regexp';
export default class Route extends Component{
    render() {
        let {path,component: Component}=this.props;
        let regexp=pathToRegexp(path,[],{end:false});
        return (
            <Consumer>
                {
                    value => {
                        let {pathname}=value.location;
                        if (regexp.test(pathname)) {
                            return <Component/>
                        } else {
                            return null;
                        }
                    }
                }
            </Consumer>
        )
    }
}
複製程式碼

6.exact 精確匹配

6.1 index.js

ReactDOM.render(
    <Router>
        <div>
+           `<Route exact={true} path="/" component={Home} />`
            <Route path="/user" component={User} />
          <Route path="/profile" component={Profile}/>
        </div>
    </Router>
,document.getElementById('root'));
複製程式碼

6.2 Route.js

src/react-router-dom/Route.js

export default class Route extends Component{
    render() {
+        `let {path,component: Component,exact=false}=this.props`;
+        `let regexp=pathToRegexp(path,[],{end:exact});`
        return (
            <Consumer>
                {
                    value => {
                        let {pathname}=value.location;
                        if (regexp.test(pathname)) {
                            return <Component/>
                        } else {
                            return null;
                        }
                    }
                }
            </Consumer>
        )
    }
}

複製程式碼

7. Link

7.1 Link.js

src/react-router-dom/Link.js

import React,{Component} from 'react';
import {Consumer} from './context';
export default class Link extends Component{
    render() {
        return (
            <Consumer>
                {
                    value => {
                        let {history: {push}}=value;
                        return (
                            <a onClick={()=>push(this.props.to)}>{this.props.children}</a>
                        )
                    }
                }
            </Consumer>
        )
    }
}
複製程式碼

7.2 index.js #

src/index.js

import 'bootstrap/dist/css/bootstrap.css'
ReactDOM.render(
    <Router>
        <div>
            <nav className="navbar navbar-inverse">
                <div className="container-fluid">
                    <div className="navbar-header">
                        <a className="navbar-brand" >學生管理系統</a>
                    </div>
                    <div id="navbar" className="collapse navbar-collapse">
                        <ul className="nav navbar-nav">
                            <li><Link to="/">Home</Link></li>
                            <li><Link to="/user">User</Link></li>
                            <li><Link to="/profile">Profile</Link></li>
                        </ul>
                    </div>
                </div>
            </nav>
            <div className="container">
                <Route exact={true} path="/" component={Home} />
                <Route path="/user" component={User} />
                <Route path="/profile" component={Profile}/>
            </div>
        </div>
    </Router>
,document.getElementById('root'));
複製程式碼

7.3 HashRouter.js

src/react-router-dom/HashRouter.js

 render() {
        let value={
            location: this.state.location,
            history: {
                push(to) {
                    window.location.hash=to;
                }
            }
        }
        return (
            <Provider value={value}>
                {this.props.children}
            </Provider>
        )
    }
複製程式碼

8.Redirect&Switch

8.1 index.js

src/index.js

<div className="container">
                <Switch>
                    <Route exact={true} path="/" component={Home} />
                    <Route path="/user" component={User} />
                    <Route path="/profile" component={Profile} />
                    <Redirect to="/"/>
                </Switch>
            </div>
複製程式碼

8.2 Redirect.js

src/react-router-dom/Redirect.js

import React,{Component} from 'react';
import {Consumer} from './context';
export default class Redirect extends Component{
    render() {
        return (
            <Consumer>
                {
                    value => {
                        value.history.push(this.props.to);
                        return null;
                    }
                }
            </Consumer>
        )
    }
}
複製程式碼

8.3 Switch.js

src/react-router-dom/Switch.js

import React,{Component} from 'react';
import {Consumer} from './context';
import pathToRegexp from 'path-to-regexp';
export default class Switch extends Component{
    render() {
        return (
            <Consumer>
                {
                    value => {
                        let {location: {pathname}}=value;
                        let children=this.props.children;
                        for (let i=0;i<children.length;i++){
                            let child=children[i];
                            let {path="/",exact=false}=child.props;
                            let regexp=pathToRegexp(path,[],{end: exact});
                            if (regexp.test(pathname)) {
                                return child;
                            }
                        }
                        return null;
                    }
                }
            </Consumer>
        )
    }
}
複製程式碼

9.頁面跳轉

9.1 User.js

src/components/User.js

<Switch>
    <Route path="/user/add" component={UserAdd} />
    <Route path="/user/list" component={UserList} />
    <Route path="/user/detail/:id" component={UserDetail}/>
</Switch>
複製程式碼

9.2 HashRouter.js

src/react-router-dom/HashRouter.js

render() {
        let value={
            location: this.state.location,
            history: {
                push(to) {
                    window.location.hash=to;
                }
            }
        }
        return (
            <Provider value={value}>
                {this.props.children}
            </Provider>
        )
}
複製程式碼

9.3 Route.js

src/react-router-dom/Route.js

import React,{Component} from 'react';
import {Consumer} from './context';
import pathToRegexp from 'path-to-regexp';
export default class Route extends Component{
    constructor(props) {
        super(props);
    }
    render() {
        let {path,component: Component,exact=false}=this.props;
        return (
            <Consumer>
                {
                    value => {
                        let {pathname}=value.location;
                        let keys=[];
                        let regexp=pathToRegexp(path,keys,{end: exact});
                        keys = keys.map(item=>item.name)
                        let result=pathname.match(regexp);
                        if (result) {
                            let [,...values]=result;
                            let match={
                                params: keys.reduce((params,key,idx) => {
                                    params[key]=values[idx];
                                    return params;
                                },{}),
                                path,
                                url: pathname
                            };

                            let props={
                                location: value.location,
                                history: value.history,
                                match
                            }
                            return <Component {...props}/>
                        } else {
                            return null;
                        }
                    }
                }
            </Consumer>
        )
    }
}
複製程式碼

9.4 Switch.js

src/react-router-dom/Switch.js

import React,{Component} from 'react';
import {Consumer} from './context';
import pathToRegexp from 'path-to-regexp';
export default class Switch extends Component{
    render() {
        return (
            <Consumer>
                {
                    value => {
                        let {location: {pathname}}=value;
                        let children=this.props.children;
                        for (let i=0;i<children.length;i++){
                            let child=children[i];
                            let {path="/",exact=false}=child.props;
                            let regexp=pathToRegexp(path,[],{end: exact});
                            if (regexp.test(pathname)) {
                                return child;
                            }
                        }
                        return null;
                    }
                }
            </Consumer>
        )
    }
}
複製程式碼

9.5 UserAdd.js

src/components/UserAdd.js

import React,{Component} from 'react';
export default class UserAdd extends Component{
    handleSubmit=(event) => {
        event.preventDefault();
        let username=this.username.value;
        let email=this.email.value;
        let user={username,email};
        let usersStr=localStorage.getItem('users');
        let users=usersStr? JSON.parse(usersStr):[];
        user.id = users.length>0? users[users.length-1].id+1:1;
        users.push(user);
        localStorage.setItem('users',JSON.stringify(users));
        this.props.history.push('/user/list');
    }
    render() {
        return (
            <div className="row">
                <div className="col-md-12">
                    <form onSubmit={this.handleSubmit}>
                        <div className="form-group">
                            <label htmlFor="username">使用者名稱</label>
                            <input type="text" className="form-control" ref={input=>this.username = input}/>
                        </div>
                        <div className="form-group">
                            <label htmlFor="email">郵箱 </label>
                            <input type="email" className="form-control" ref={input=>this.email = input}/>
                        </div>
                        <div className="form-group">
                            <input type="submit" className="btn btn-primary"/>
                        </div>
                    </form>
                </div>
            </div>
        )
    }
}
複製程式碼

9.6 UserDetail.js

src/components/UserDetail.js

import React,{Component} from 'react';
export default class UserList extends Component{
    state={
        user: {}
    }
    componentDidMount() {
        let usersStr=localStorage.getItem('users');
        let users=usersStr? JSON.parse(usersStr):[];
        let user = users.find(user => user.id==this.props.match.params.id);
        this.setState({user});
    }    
    render() {
        let {user}=this.state;
        return (
            <div className="row">
                <div className="col-md-12">
                    <div>ID:{user.id}</div>
                    <div>使用者名稱:{user.username}</div>
                    <div>郵箱:{user.email}</div>
                </div>
            </div>
        )
    }
}
複製程式碼

9.7 UserList.js

src/components/UserList.js

import React,{Component} from 'react';
import {Link} from '../react-router-dom';
export default class UserList extends Component{
    state={
        users:[]
    }
    componentDidMount() {
        let usersStr=localStorage.getItem('users');
        let users=usersStr? JSON.parse(usersStr):[];
        this.setState({users});
    }
    render() {
        return (
            <div className="row">
                <div className="col-md-12">
                    <ul className="list-group">
                        {
                            this.state.users.map(user => (
                                <li className="list-group-item" key={user.id}>
                                    <Link to={`/user/detail/${user.id}`}>{user.username}</Link>
                                </li>
                            ))
                        }
                    </ul>
                </div>
            </div>
        )
    }
}
複製程式碼

###10. 受保護的路由

10.1 src/index.js

         <Switch>
                    <Route exact={true} path="/" component={Home} />
                    <Route path="/user" component={User} />
+                   <Route path="/login" component={Login} />
+                    <Protected path="/profile" component={Profile} />`
                    <Redirect to="/"/>
                </Switch>
複製程式碼

10.2 HashRouter.js src/react-router-dom/HashRouter.js

render() {
        let self=this;
        let value={
            location: self.state.location,
            history: {
+                push(to) {
+                    if (typeof to == 'object') {
+                        let {pathname,state}=to;
+                        self.setState({
+                            location: {
+                                ...self.state.location,state,pathname
+                            }
+                        },() => {
+                            window.location.hash=pathname;
+                        });
+                    } else {
+                        window.location.hash=to;
+                    }                    
+                }
+            }
        }
        return (
            <Provider value={value}>
                {this.props.children}
            </Provider>
        )
    }
複製程式碼

10.3 Route.js

src/react-router-dom/Route.js

+                             if (Component) {
+                                return <Component {...props}/> 
+                            } else if (render) {
+                                return render(props);
+                            } else {
+                                return null;
+                            }
複製程式碼

10.4 Login.js

src/components/Login.js

+ import React from 'react'
+ export default class Login extends React.Component{
+    handleClick=() => {
+        localStorage.setItem('logined',true);
+        this.props.history.push(this.props.location.state.from);
+    }
+    render() {
+        return (
+            <div>
+                <button
+                    className="btn btn-primary"
+                    onClick={this.handleClick}
+                >登入</button>
+            </div>
+        )
+    }
+}
複製程式碼

10.5 Protected.js

src/components/Protected.js

+ import React from 'react'
+ import {Route,Redirect} from '../react-router-dom';
+ export default ({component:Component,...rest}) => (
+     <Route
+         {...rest}
+         render={
+             props => (
+                 localStorage.getItem('logined')?
+                     <Component {...props} />:
+                     <Redirect to={{pathname: '/login',state: {from: props.location.pathname}}} />
+             )
+         }
+     />
+ )
複製程式碼

11. 自定義導航

11.1 index.js

src/index.js

<ul className="nav navbar-nav">
+    <MenuLink exact to="/">Home</MenuLink>
+    <MenuLink to="/user">User</MenuLink>
+    <MenuLink to="/profile">Profile</MenuLink>
</ul>
複製程式碼

11.2 Route.js

src/react-router-dom/Route.js

let props={
                            location: value.location,
                            history: value.history
                        }
                        if (result) {
                            let [,...values]=result;
                            let match={
                                params: keys.reduce((params,key,idx) => {
                                    params[key]=values[idx];
                                    return params;
                                },{}),
                                path,
                                url: pathname
                            };

                            props.match = match;
                            if (Component) {
                                return <Component {...props}/> 
                            } else if (render) {
                                return render(props);
                            } else if(children){
                                return children(props);
                            }else {
                              return null;
                            }
                        } else if(children){
                            return children(props);
                        } else {
                            return null;
                        }
複製程式碼

11.3 MenuLink.js

import React from 'react'
import {Route,Link} from '../react-router-dom';
import './MenuLink.css'
export default ({to,exact,children}) => (
    <Route
        path={to}
        exact={exact}
        children={
            props => (
                <li className={props.match?'active':''}><Link to={to}>{children}</Link></li>
            )
        }
    />
)
複製程式碼

11.4 MenuLink.css

.navbar-inverse .navbar-nav > .active > a{
    background-color: orange;
}
複製程式碼

12. 防止跳轉

12.1 UserAdd.js

src/components/UserAdd.js

import React,{Component} from 'react';
import {Prompt} from '../react-router-dom';
export default class UserAdd extends Component{
+    state={
+        isBlocking:false
+    }
    handleSubmit=(event) => {
+        event.preventDefault();
+        this.setState({
+            isBlocking:false
+        },() => {
            let username=this.username.value;
            let email=this.email.value;
            let user={username,email};
            let usersStr=localStorage.getItem('users');
            let users=usersStr? JSON.parse(usersStr):[];
            user.id = users.length>0? users[users.length-1].id+1:1;
            users.push(user);
            localStorage.setItem('users',JSON.stringify(users));
            this.props.history.push('/user/list');
        });
    }
    render() {
+        let {isBlocking}=this.state;
        return (
            <div className="row">
                <div className="col-md-12">
                    <form onSubmit={this.handleSubmit}>
+                        <Prompt
+                            when={isBlocking}
+                            message={
+                                location=>`你確定要跳轉到${location.pathname}嗎?`
+                            }
+                        />
                        <div className="form-group">
                            <label htmlFor="username">使用者名稱</label>
                            <input type="text"
                                onChange={
                                    event => this.setState({isBlocking:event.target.value.length>0})
                                }
                                className="form-control" ref={input => this.username=input} />
                        </div>
                        <div className="form-group">
                            <label htmlFor="email">郵箱 </label>
                            <input
                                onChange={
                                    event => this.setState({isBlocking:event.target.value.length>0})
                                }
                                type="email" className="form-control" ref={input => this.email=input} />
                        </div>
                        <div className="form-group">
                            <input type="submit" className="btn btn-primary"/>
                        </div>
                    </form>
                </div>
            </div>
        )
    }
}
複製程式碼

12.2 HashRouter.js

src/react-router-dom/HashRouter.js

let value={
            location: self.state.location,
            history: {
                push(to) {
+                    if (self.block) {
+                        let allow=window.confirm(self.block(typeof to=='object'? to:{pathname:to}));
+                        if (!allow) return;
+                    }
                    if (typeof to == 'object') {
                        let {pathname,state}=to;
                        self.setState({
                            location: {
                                ...self.state.location,state,pathname
                            }
                        },() => {
                            window.location.hash=pathname;
                        });
                    } else {
                        window.location.hash=to;
                    }

                },
+                block(message) {
+                    self.block=message;
+                },
+                unblock() {
+                    self.block=null;
+                }
            }
        }
複製程式碼

12.3 Prompt.js

src/react-router-dom/Prompt.js

import React from 'react'
import {Consumer} from './context'
export default class Prompt extends React.Component{
    componentWillUnmount() {
        this.history.unblock();
    }
    render() {
        return (
            <Consumer>
                {
                    value => {
                        this.history=value.history;
                        const {when,message}=this.props;
                        if (when) {
                            this.history.block(message);
                        } else {
                            this.history.block(null);
                        }
                    }
                }
            </Consumer>
        );
    }
}
複製程式碼

13. withRouter

13.1 index.js

rc/index.js

    <div className="container-fluid">
+           <Header/>
            <div id="navbar" className="collapse navbar-collapse">
複製程式碼

13.2 Route.js

src/react-router-dom/Route.js

+    let {path='/',component: Component,exact=false,render,children}=this.props;
    return (
        <Consumer>
複製程式碼

13.3 index.js

src/react-router-dom/index.js

+import withRouter from './withRouter';
export {
    HashRouter,
    Route,
    Link,
    Redirect,
    Switch,
    Prompt,
+    withRouter
}
複製程式碼

13.4 Header.js

src/components/Header.js

import React from 'react'
import {withRouter} from '../react-router-dom';
class Header extends React.Component{
    render() {
        return (
            <div className="navbar-header">
                <a
                    onClick={()=>this.props.history.push('/')}
                    className="navbar-brand" >
                   學生管理系統
                </a>
            </div>
        )
    }
}
export default withRouter(Header);
複製程式碼

13.5 withRouter.js

src/react-router-dom/withRouter.js

import React from 'react'
import {Consumer} from './context';
import Route from './Route';
export default function (Component) {
    class Proxy extends React.Component{
        render() {
            return (
                <Consumer>
                    {
                        value => {
                            return <Route  component={Component}/>
                        }
                    }
               </Consumer>
            )
        }
    }
    return Proxy;
}
複製程式碼

相關文章