清明时节雨纷纷,不如在家撸代码。从零开始实现一个react-router,并跑通react-router-dom里的example。node
react-router是作SPA(不是你想的SPA)时,控制不一样的url渲染不一样的组件的js库。用react-router能够方便开发,不须要手动维护url和组件的对应关系。开发时用react-router-dom,react-router-dom里面的组件是对react-router组件的封装。react
单页应用的原理用两种,一种是经过hash的变化,改变页面,另外一种是经过url的变化改变页面。git
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的数据解构,子组件屡次用到里面的方法或属性,方便复用。github
//ChildContext的数据解构
{
router:{
history, //某种 history
route:{
location:history.location,
match:{} //匹配到的结果
}
}
}
复制代码