Open libin1991 opened 6 years ago
清明时节雨纷纷,不如在家撸代码。从零开始实现一个react-router,并跑通react-router-dom里的example。
react-router是做SPA(不是你想的SPA)时,控制不同的url渲染不同的组件的js库。用react-router可以方便开发,不需要手动维护url和组件的对应关系。开发时用react-router-dom,react-router-dom里面的组件是对react-router组件的封装。
单页应用的原理用两种,一种是通过hash的变化,改变页面,另一种是通过url的变化改变页面。
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; };
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}/>; } }
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; } }
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; } }
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> ); } }
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; } };
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; } };
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:{} //匹配到的结果 } } }
react-router是做SPA(不是你想的SPA)时,控制不同的url渲染不同的组件的js库。用react-router可以方便开发,不需要手动维护url和组件的对应关系。开发时用react-router-dom,react-router-dom里面的组件是对react-router组件的封装。
SPA的原理
单页应用的原理用两种,一种是通过hash的变化,改变页面,另一种是通过url的变化改变页面。
React-Router-dom的核心组件
history
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
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
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
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
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
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
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
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:{} //匹配到的结果 } } }
参考