打开 react-router-dom 源码会发现 它自身并没有很多东西,大部分都来自 react-router 和 history 库。
- react-router-dom 放跟 dom 有关的
- react-router 放跟 dom 无关的
import { match } from 'react-router';import * as React from 'react';import * as H from 'history';
我们这里直接在 src 下创建3个文件夹 react-router-dom、react-router、history
实现 基本路由

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><Route exact path="/" component={Home} /><Route path="/user" component={User} /><Route path="/profile" component={Profile} /></Router>,document.getElementById('root'));
src/react-router-dom
src/react-router-dom/index.js
export * from '../react-router';export { default as HashRouter } from './HashRouter'export { default as BrowserRouter } from './BrowserRouter'
src/react-router-dom/HashRouter.js
import React from 'react';import { Router } from '../react-router';// 创建一个 hash history 对象的方法,模拟一个自己的 history 对象,但是是用 hash 实现的import { createHashHistory } from 'history';class HashRouter extends React.Component {history = createHashHistory(); // hash实现render(){return (<Router history={this.history}>{this.props.children}</Router>)}}export default HashRouter;
src/react-router-dom/BrowserRouter.js
import React from 'react';import { Router } from '../react-router';import { createBrowserHistory } from 'history';class BrowserRouter extends React.Component {// 创建相应的历史对象 history,并且传入Router组件,原样渲染子组件// 不管是哪种创建历史对象的方法,得到的history 长的都一样,都像window.historyhistory = createBrowserHistory(this.props); //window.historyrender(){return (<Router history={this.history}>{this.props.children}</Router>)}}export default BrowserRouter;
src/react-router
src/react-router/index.js
export { default as Router } from './Router';export { default as Route } from './Route';export { default as __RouterContext } from './RouterContext';
src/react-router/RouterContext.js
import React from 'react';const RouterContext = React.createContext();export default RouterContext;
src/react-router/Router.js
import React from 'react';import RouterContext from './RouterContext';class Router extends React.Component {constructor(props){super(props);this.state = {location: props.history.location,}// 监听历史对象中的路径变化,当路径发生变化后执行回调函数,参数就是最新的路径对象// history.listen 函数会返回一个 取消监听 的函数,我们可以在组件卸载的时候,调用它this.unlisten = props.history.listen(location => {this.setState({location})});}componentWillUnmount(){this.unlisten && this.unlisten();}render(){// 通过value向下层传递数据let value = {location: this.state.location, //用于传递给 Route ,来判断路由是否匹配history: this.props.history, //HashRouter、BrowserHistory 组件给的}return (<RouterContext.Provider value={value}>{this.props.children}</RouterContext.Provider>)}}export default Router;
src/react-router/Route.js
import React from 'react';import RouterContext from './RouterContext';/*** 1.获取 context 的值* 2.匹配路由规则里的 path 是否和当前地址中的 url 地址是否相等,* 如果相等,就渲染 component;如果不相等,就不渲染任何东西。*/class Route extends React.Component {static contextType = RouterContext;render(){const {context, props} = this;const {history, location} = context;const {exact, path, component: RouteComponent, render} = props; //路由挂载属性const match = location.pathname === path; //路由规则是否匹配(match对象后边再实现)const routerProps = {history, location}; //路由属性 history、location、matchlet element = null;// 优先级 RouteComponent > render(匹配才渲染) > children(匹不匹配都渲染)if (match){routerProps.match = match;if (RouteComponent) {element = <RouteComponent {...routerProps} />;} else if (render){element = render(routerProps);} else if (children){element = children(routerProps);}} else {if (children) element = children(routerProps);}return element;}}export default Route;
实现 history
src/history/index.js
export {default as createHashHistory} from './createHashHistory';export {default as createBrowserHistory} from './createBrowserHistory';
src/history/createHashHistory.js
未处理完
function parsePath(str=window.location.hash){str = str || '#/';let hash = '', pathname = '', search = '';let searchIndex = str.indexOf('?');if (searchIndex > -1){search = str.slice(searchIndex);pathname = str.slice(1, searchIndex);} else {pathname = str.slice(1);}return {hash, pathname, search};}function stringifyPath(params={}){let {pathname, search} = params;let result = pathname;if (search){result += `${search.charAt(0) === '?' ? '' : '?'}${search}`}return result;}/*** 1.state 的处理 自己维护state* 2.历史栈的维护 自己维护一个栈* 3.浏览器自身的前进后退按钮暂时未考虑*/function createHashHistory(){let stack = []; //历史栈let stackIndex = 0; //栈指针let action; //当前最后一个动作是什么动作,push 'PUSH'; go 'POP';let state; //状态let listeners = []; //存放 监听函数 的数组,在路由变化后,遍历里面的每一个监听函数let location = parsePath();location.state = state;window.location.hash = window.location.hash ? window.location.hash.slice(1) : '/';;// 参数 listener 是监听函数function listen(listener){listeners.push(listener);return () => { // 返回一个销毁函数,用于取消监听listeners = listeners.filter(item => item !== listener)}}// 监听页面路由变化setTimeout(() => {window.addEventListener('hashchange', () => {let location = parsePath();Object.assign(history, {action, location: {...location, state}});if (action === 'PUSH'){stack[++stackIndex] = history.location;} else if (action === 'REPLACE'){stack[stackIndex] = history.location;}// debugger;// action = '';listeners.forEach(item => item(history.location));})})function push(pathname, nextState){action = 'PUSH';if (typeof pathname === 'object'){state = pathname.state;pathname = stringifyPath(pathname);} else {state = nextState;}window.location.hash = pathname;}function replace(pathname, nextState){action = 'REPLACE';if (typeof pathname === 'object'){state = pathname.state;pathname = stringifyPath(pathname);} else {state = nextState;}window.location.hash = pathname;}function go(n){action = 'POP';stackIndex += n;let nextLocation = stack[stackIndex];state = nextLocation.state;window.location.hash = stringifyPath(nextLocation);}function goBack(){go(-1);}function goForward(){go(1);}const history = {action: 'POP',// block,// createHref,go,goBack,goForward,// length,listen,location,push,replace,}stack[stackIndex] = history.location;return history;}export default createHashHistory;
src/history/createBrowserHistory
import resolvePathname from 'resolve-pathname';const PopStateEvent = 'popstate'; //h5 history API中的监听事件// const HashChangeEvent = 'hashchange'; //监听hash变化// 添加首位斜杠function addLeadingSlash(path){return path.charAt(0) === '/' ? path : `/${path}`;}// 移除首位斜杠// function stripLeadingSlash(path) {// return path.charAt(0) === '/' ? path.substr(1) : path;// }// 移除末尾斜杠function stripTrailingSlash(path) {return path.charAt(path.length - 1) === '/' ? path.slice(0, -1) : path;}// 检测是否有basenamefunction hasBasename(path, prefix) {return path.toLowerCase().indexOf(prefix.toLowerCase()) === 0 && '/?#'.indexOf(path.charAt(prefix.length)) !== -1;}// 移除basenamefunction stripBasename(path, prefix) {return hasBasename(path, prefix) ? path.substr(prefix.length) : path;}function getHistoryState(){try {return window.history.state || {};} catch (e) {// IE 11 sometimes throws when accessing window.history.state// See https://github.com/ReactTraining/history/pull/289return {};}}// 解析 path 路径function parsePath(path){let pathname = path || '/';let search = '';let hash = '';let hashIndex = pathname.indexOf('#');if (hashIndex !== -1){hash = pathname.slice(hashIndex);pathname = pathname.slice(0, hashIndex);hash = hash === '#' ? '' : hash;}let searchIndex = pathname.indexOf('?');if (searchIndex !== -1){search = pathname.slice(searchIndex);pathname = pathname.slice(0, searchIndex);search = search === '?' ? '' : search;}return {pathname, search, hash};}// 根据 location 对象,创建 path 路径function createPath(location={}){let {pathname, search, hash} = location;let path = pathname || '/';if (search && search !== '?') path += (search.charAt(0) === '?' ? search : `?${search}`);if (hash && hash !== '#') path += (hash.charAt(0) === '#' ? hash : `#${hash}`);return path;}/** 创建新的 location 对象** @param {*} path 路径 string | object* @param {*} state state数据* @param {*} key key唯一标识符* @param {*} currentLocation 当前的 location 对象* @returns*/function createLocation(path, state, key, currentLocation){let location;if (typeof path === 'string'){location = parsePath(path);location.state = state;} else {location = Object.assign({}, path);if (location.pathname === undefined) location.pathname = '';if (location.search){if (location.search.charAt(0) !== '?') location.search = `?${location.search}`;} else {location.search = '';}if (location.hash){if (location.hash.charAt(0) !== '#') location.hash = `#${location.hash}`;} else {location.hash = '';}if (state !== undefined && location.state === undefined) location.state = state;}try {location.pathname = decodeURI(location.pathname);} catch(e){throw e;}if (key) location.key = key;if (currentLocation){if (!location.pathname){location.pathname = currentLocation.pathname;} else if(location.pathname.charAt(0) !== '/'){location.pathname = resolvePathname(location.pathname, currentLocation.pathname);}} else {if (!location.pathname) location.pathname = '/';}return location;}function createTransitionManager(){let prompt = null;// 设置 promptfunction setPrompt(nextPrompt){prompt = nextPrompt;// 返回一个销毁函数return () => {if (prompt === nextPrompt) prompt = null;}}// 跳转拦截function confirmTransitionTo(location, action, getUserConfirmation, callback) {if (prompt !== null){let result = typeof prompt === 'function' ? prompt(location, action) : prompt;if (typeof result === 'string'){if (typeof getUserConfirmation === 'function'){getUserConfirmation(result, callback);} else {callback(true);}} else {callback(result !== false);}} else {callback(true);}}// 添加监听let listeners = []; //存放 监听函数 的数组,在路由变化后,遍历里面的每一个监听函数function appendListener(fn){let isActive = true;function listener(...args){if (isActive) fn.apply(void 0, args);}listeners.push(listener);// 返回一个销毁函数,用于取消监听return () => {isActive = false;listeners = listeners.filter(item => item !== listener);}}// 触发监听function notifyListeners(...args){listeners.forEach(listener => listener.apply(void 0, args));}return {setPrompt,confirmTransitionTo,appendListener,notifyListeners,};}// 默认跳转拦截函数function getConfirmation(message, callback) {callback(window.confirm(message)); // eslint-disable-line no-alert}function isExtraneousPopstateEvent(event) {return event.state === undefined && navigator.userAgent.indexOf('CriOS') === -1;}/** 创建 browser history 对象* Creates a history object that uses the HTML5 history API including* pushState, replaceState, and the popstate event.** @param {*} props.basename 基链接* @param {*} props.keylength key长度* @param {*} props.forceRefresh 是否强制刷新* @param {*} props.getUserConfirmation 自定义跳转拦截函数* @returns history 对象*/function createBrowserHistory(props={}){const globalHistory = window.history; //h5 history 全局对象let {keyLength=6, forceRefresh, getUserConfirmation=getConfirmation} = props;let basename = props.basename ? stripTrailingSlash(addLeadingSlash(props.basename)) : '';// 根据 window.location 创建 location 对象function getDOMLocation(historyState){let ref = historyState || {};let {key, state} = ref;let _location = window.location;let {pathname, search, hash} = _location;let path = pathname + search + hash;if (basename) path = stripBasename(path, basename);return createLocation(path, state, key);}// 创建keyfunction createKey() {return Math.random().toString(36).substr(2, keyLength);}let transitionManager = createTransitionManager();// 更新 history,并触发一次监听函数function setState(nextState){Object.assign(history, nextState);history.length = globalHistory.length;transitionManager.notifyListeners(history.location, history.action);}let initialLocation = getDOMLocation(getHistoryState());let allKeys = [initialLocation.key]; // 存储 历史页面的keyfunction createHref(location){return `${basename}${createPath(location)}`;}function push(path, nextState){let action = 'PUSH';let location = createLocation(path, nextState, createKey(), history.location);transitionManager.confirmTransitionTo(location, action, getUserConfirmation, ok => {if (!ok) return; //如果确认拦截,直接返回,不进行跳转。let href = createHref(location);let {key, state} = location;globalHistory.pushState({key, state}, null, href);if (forceRefresh){window.location.href = href;} else {let prevIndex = allKeys.indexOf(history.location.key);let nextKeys = allKeys.slice(0, prevIndex + 1);nextKeys.push(key);allKeys = nextKeys;setState({action, location});}})}function replace(path, nextState){let action = 'REPLACE';let location = createLocation(path, nextState, createKey(), history.location);transitionManager.confirmTransitionTo(location, action, getUserConfirmation, ok => {if (!ok) return; //如果确认拦截,直接返回,不进行跳转。let href = createHref(location);let {key, state} = location;globalHistory.replaceState({key, state}, null, href);if (forceRefresh){window.location.replace(href);} else {let prevIndex = allKeys.indexOf(history.location.key);if (prevIndex !== -1) allKeys[prevIndex] = key;setState({action, location});}})}function go(n){globalHistory.go(n);}function goBack(){go(-1);}function goForward(){go(1);}function handlePopState(event){if (isExtraneousPopstateEvent(event)) return;handlePop(getDOMLocation(event.state));}let forceNextPop = false;function handlePop(location){if (forceNextPop){forceNextPop = false;setState();} else {let action = 'POP';transitionManager.confirmTransitionTo(location, action, getUserConfirmation, ok => {if (ok){setState({action, location});} else {revertPop(location);}})}}//如果确认拦截,要回退回原路由function revertPop(fromLocation){let toLocation = history.location;let toIndex = allKeys.indexOf(toLocation.key);if (toIndex === -1) toIndex = 0;let fromIndex = allKeys.indexOf(fromLocation.key);if (fromIndex === -1) fromIndex = 0;let delta = toIndex - fromIndex; //如果不为0,说明发生了前进或后退if (delta){forceNextPop = true;go(delta);}}// 监听事件的添加与移除let listenerCount = 0; //已绑定的监听函数数量function checkDOMListeners(delta){listenerCount += delta;// 当回退或前进的时候,会执行 popstate 事件。(popstate事件是浏览器自带的,默认支持)if (listenerCount === 1 && delta === 1){window.addEventListener(PopStateEvent, handlePopState);} else if (listenerCount === 0){window.removeEventListener(PopStateEvent, handlePopState);}}function listen(listener){let unlisten = transitionManager.appendListener(listener);checkDOMListeners(1);return () => {checkDOMListeners(-1);unlisten();}}// 跳转拦截let isBlocked = false; //是否阻止跳转function block(prompt){if (prompt === void 0) prompt = false;let unblock = transitionManager.setPrompt(prompt);if (!isBlocked){checkDOMListeners(1);isBlocked = true;}// 返回一个用于清空拦截的函数return () => {if (isBlocked){isBlocked = false;checkDOMListeners(-1);}return unblock();}}const history = {length: globalHistory.length,action: 'POP',location: initialLocation,createHref,push,replace,go,goBack,goForward,block,listen,}return history;}export default createBrowserHistory;
path-to-regexp 介绍
- https://www.yuque.com/zhuchaoyang/wrif6k/grhxy3/
- regulex 正则可视化工具
- path-to-regexp 路径匹配库
- sensitive 是否大小写敏感 (默认值: false)
- strict 是否允许结尾有一个可选的/ (默认值: false)
- end 是否匹配整个字符串 (默认值: true)
/home结束
let pathToRegExp = require('path-to-regexp');let regExp = pathToRegExp('/home', [], {end:true});console.log(regExp); // /^\/home\/?$/iconsole.log(regExp.test('/home')); //trueconsole.log(regExp.test('/home/2')); //false

/home非结束
let pathToRegExp = require('path-to-regexp');let regExp = pathToRegExp('/home', [], {end:false});console.log(regExp); // /^\/home\/?(?=\/|$)/iconsole.log(regExp.test('/home')); //trueconsole.log(regExp.test('/home/')); //trueconsole.log(regExp.test('/home//')); //trueconsole.log(regExp.test('/home/2')); //true

路径参数
let pathToRegExp = require('path-to-regexp');let regExp = pathToRegExp('/user/:id', [], {end: false});console.log(regExp); // /^\/user\/(?:([^\/]+?))\/?(?=\/|$)/iconsole.log(regExp.test('/user/1')); //trueconsole.log(regExp.test('/user/1/')); //trueconsole.log(regExp.test('/user/1//')); //trueconsole.log(regExp.test('/user/1/avatar')); //true

正则匹配
更多正则知识见 js-正则篇:https://www.yuque.com/zhuchaoyang/wrif6k/grhxy3
是否捕获 | 表达式 | 含义 | | —- | —- | | () | 表示捕获分组,()会把每个分组里的匹配的值保存起来,使用$n(n是一个数字,表示第n个捕获组的内容) | | (?:) | 表示非捕获分组,和捕获分组唯一的区别在于,非捕获分组匹配的值不会保存起来 | | (?…) | 表示命名捕获分组,反向引用一个命名分组的语法是 \k ,在 replace() 方法的替换字符串中反向引用是用 $ |
前瞻和后顾 | exp(?=pattern) | 正向肯定查找(前瞻),后面必须跟着什么 | | —- | —- | | exp(?!pattern) | 正向否定查找(前瞻),后面不能跟着什么 | | exp(?<=pattern) | 反向肯定条件查找(后顾),不捕获 | | exp(?<!pattern) | 反向否定条件查找(后顾) |
//会消耗掉字符的//console.log('1a'.match(/\d[a-z][a-z]/));//?= 正向肯定查找 不消费字符 正向前瞻//console.log('1a'.match(/\d(?=[a-z])[a-z]/));//匹配分组捕获console.log('1ab'.match(/1([a-z])([a-z])/)); //[ '1ab', 'a', 'b' ]//非捕获分组console.log('1ab'.match(/1(?:[a-z])([a-z])/)); //[ '1ab', 'b' ]//正向肯定前瞻 (?=[a-z]) 只是一个判断条件,只匹配右边(后边)紧跟小写字母的数字console.log('1a'.match(/\d(?=[a-z])[a-z]/)); //[ '1a' ]//正向否定前瞻 (?![A-z]) 只匹配右边紧跟不是大写字母的数字console.log('1a'.match(/\d(?![A-Z])[a-z]/)); //[ '1a' ]//反向肯定前瞻 (?<=[a-z]) 只匹配左边(前边)是小写字母的数字console.log('1a'.match(/(?<=[a-z])\d[a-z]/)); //nullconsole.log('b1a'.match(/(?<=[a-z])\d[a-z]/)); //[ '1a' ]//反向否定前瞻 (?<![A-Z]) 只匹配左边不是大写字母的数字console.log('1a'.match(/(?<![A-Z])\d[a-z]/)); //[ '1a' ]console.log('B1a'.match(/(?<![A-Z])\d[a-z]/)); //null
- 命名捕获分组
```javascript
console.log(/(?
\d{2})-(? \d{2})/.exec(‘kx11-22aBc’)); // console.log(‘kx11-22aBc’.match(/(? \d{2})-(? \d{2})/)); // (? \d{2}) 第一个分组起名叫 x // (? \d{2}) 第二个分组起名叫 y
var result = [“11-22”, “11”, “22”]; //匹配的字符串 第一个分组 第二个分组 result.index = 2; //匹配字符串的索引 (“kx11-22aBc”).indexOf(‘11-22’) result.input = “kx11-22aBc”; //原字符串 result.groups = {x: “11”, y: “22”};
console.log(‘11-11’.match(/(?
console.log(‘11-22’.replace(/(?
<a name="jLKTG"></a># 实现 matchPath<a name="O8o15"></a>## src/react-router/index.js```javascriptexport { default as Router } from './Router';export { default as Route } from './Route';export { default as __RouterContext } from './RouterContext';+ export { default as matchPath } from './matchPath';
src/react-router/Router.js
import React from 'react';import RouterContext from './RouterContext';class Router extends React.Component {+ static computeRootMatch(pathname){+ return { path: '/', url: '/', params: {}, isExact: pathname === '/' }+ }constructor(props){super(props);this.state = {location: props.history.location,}// 监听历史对象中的路径变化,当路径发生变化后执行回调函数,参数就是最新的路径对象// history.listen 函数会返回一个 取消监听 的函数,我们可以在组件卸载的时候,调用它this.unlisten = props.history.listen(location => {this.setState({location})});}componentWillUnmount(){this.unlisten && this.unlisten();}render(){//通过value向下层传递数据let value = {location: this.state.location, //用于传递给 Route ,来判断路由是否匹配history: this.props.history, //HashRouter、BrowserHistory 组件给的+ match: Router.computeRootMatch(this.state.location.pathname), //根 match}return (<RouterContext.Provider value={value}>{this.props.children}</RouterContext.Provider>)}}export default Router;
src/react-router/Route.js
import React from 'react';import RouterContext from './RouterContext';+ import matchPath from './matchPath';class Route extends React.Component {static contextType = RouterContext;render(){const {context, props} = this;const {history, location} = context;+ const {component: RouteComponent, render} = props; //路由挂载属性+ const match = matchPath(location.pathname, props); //路由匹配const routerProps = {history, location}; //路由属性 history、location、matchlet element = null;// 优先级 RouteComponent > render(匹配才渲染) > children(匹不匹配都渲染)if (match){routerProps.match = match;if (RouteComponent) {element = <RouteComponent {...routerProps} />;} else if (render){element = render(routerProps);} else if (children){element = children(routerProps);}} else {if (children) element = children(routerProps);}// 重写Provide的value值+ return (+ <RouterContext.Provider value={routerProps}>+ {element}+ </RouterContext.Provider>+ );}}export default Route;
src/react-router/matchPath.js
const pathToRegExp = require('path-to-regexp');const cache = {}; //正则匹配路径缓存// 获取路径匹配正则表达式function compilePath(path, options){const cacheKey = `${path}${JSON.stringify(options)}`;if (cache[cacheKey]) return cache[cacheKey];const keys = [];const regExp = pathToRegExp(path, keys, options);let result = {keys, regExp};cache[cacheKey] = result;return result;}/** 路由匹配方法** @param {*} pathname 浏览器中当前的真实的路径名* @param {*} options 其实就是 Route组件 的属性* path Route组件传递的原路径* exact 是否精确匹配(是否匹配整个字符串,后面不能跟子路径) /home /home/aa* strict 是否严格匹配(是否允许结尾有一个可选的/ ) /home/ /home* sensitive 是否大小写敏感 /home /HOME*/function matchPath(pathname, options={}){const {path="/", exact=false, strict=false, sensitive=false} = options;const {keys, regExp} = compilePath(path, {end: exact, strict, sensitive});const match = regExp.exec(pathname);// 如果不匹配,返回 nullif (!match) return null;const [url, ...values] = match;const isExact = pathname === url;// 如果要求精确,但是不精确,也返回 nullif (exact && !isExact) return null;const params = keys.reduce((obj, key, index) => {obj[key.name] = values[index];return obj;}, {})return {isExact, params, path, url};}export default matchPath;
实现 Switch
src/index.js demo测试
import React from 'react';import ReactDOM from 'react-dom';import { BrowserRouter as Router, Route, Switch} from './react-router-dom';ReactDOM.render(<Router basename="/m"><Switch><Route exact path="/" component={Home} /><Route path="/about" component={<div>about</div>} /><Route path="/:user" render={props => <div>页面:{props.match.params.user}</div>} /><Route render={props => <div>未发现路由页面</div>} /></Switch></Router>,document.getElementById('root'));
src/react-router/index.js
+ export { default as Switch } from './Switch';
src/react-router/Route.js
class Route extends React.Component {static contextType = RouterContext;render(){let element = null;const {context, props: ps} = this;const {history, location} = context;+ const {component: RouteComponent, render, computedMatch} = ps; //路由挂载属性+ const match = computedMatch ? computedMatch : matchPath(location.pathname, ps); //路由匹配const routerProps = {history, location, match};...}}export default Route;
src/react-router/Switch.js
import React from 'react';import RouterContext from './RouterContext'import matchPath from './matchPath'class Switch extends React.Component {static contextType = RouterContext;render (){const {context, props: ps} = this;const {location} = context;let element, match;// 因为 this.props.children 可以是 undefined,可以是对象,可以是数组,也可能是字符串或数字;// 所以不能直接用 this.props.children.forEach,React.children.forEach 帮我们做了兼容性处理React.Children.forEach(ps.children, child => {// 只要有一个率先匹配上,后边的路由就不再走了。if (!match && React.isValidElement(child)){element = child;match = matchPath(location.pathname, child.props);}})return match ? React.cloneElement(element, {computedMatch: match}) : null;}}export default Switch;
实现 Redirect
src/react-router.js
export { default as Redirect } from './Redirect';
src/react-router/ Redirect.js 简易版
import React from 'react';import Lifecycle from './Lifecycle';import RouterContext from './RouterContext';function Redirect({from, to, exact=false, strict=false}){return (<RouterContext.Consumer>{({history}) => {return (<Lifecycle onMount={() => history.push(to)} />);}}</RouterContext.Consumer>)}export default Redirect;
src/react-router/Lifecycle.js 创建一个生命周期类,不返回东西,只处理逻辑
import React from 'react';class Lifecycle extends React.Component {componentDidMount(){this.props.onMount && this.props.onMount(this);}componentWillUnmount(){this.props.onUnmount && this.props.onUnmount(this);}render (){return null;}}export default Lifecycle;
实现 Link
src/react-router-dom/index.js
export { default as Link } from './Link'
src/react-router-dom/Link.js
import React from 'react';import { __RouterContext as RouterContext } from '../react-router';function Link (props){return (<RouterContext.Consumer>{({history}) => (<a {...props} href="" onClick={(event) => {event.preventDefault(); //阻止 a标签 的默认事件history.push(props.to);}}>{props.children}</a>)}</RouterContext.Consumer>)}export default Link;
实现 NavLink
demo: https://www.yuque.com/zhuchaoyang/cx9erp/gm2lb9#ehYUV
src/react-router-dom/index.js
export { default as NavLink } from './NavLink'
src/react-router-dom/NavLink.js
源码的实现方式
import React from 'react';import { __RouterContext as RouterContext, matchPath } from '../react-router';import { Link } from './'function NavLink (props){let context = React.useContext(RouterContext);const {to, //匹配的路径className: classNameProp = '', //原生类名activeClassName='', //激活类名style: styleProp = {}, //原始样式activeStyle={}, //激活样式children,exact,strict,} = props;// 当前的路径pathname 和 to路径是否匹配const path = typeof to === 'object' ? to.pathname : to;let isActive = matchPath(context.location.pathname, {path, exact, strict});let className = isActive ? joinClassName(classNameProp, activeClassName) : classNameProp;let style = isActive ? {...styleProp, ...activeStyle} : styleProp;let linkProps = { className, style, to, children };return <Link {...linkProps} />}function joinClassName(...classNames){return classNames.filter(c => c).join(' '); //过滤掉空字符串}export default NavLink;
利用 Route 组件的 children 实现
import React from 'react';import { Route } from '../react-router';import { Link } from './'function NavLink(props){const {to, //匹配的路径className: classNameProp = '', //原生类名activeClassName='', //激活类名style: styleProp = {}, //原始样式activeStyle={}, //激活样式children,exact,strict,} = props;return (<Routepath={typeof to === 'object' ? to.pathname : to}exact={exact}strict={strict}children={({match: isActive}) => {let className = isActive ? joinClassName(classNameProp, activeClassName) : classNameProp;let style = isActive ? {...styleProp, ...activeStyle} : styleProp;let linkProps = { className, style, to, children };return <Link {...linkProps} />}} />)}export default NavLink;
实现 withRouter
src/react-router/index.js
export { default as withRouter } from './withRouter';
src/react-router/withRouter.js
import React from 'react';import RouterContext from './RouterContext';function withRouter(OldComponent){return props => {return (<RouterContext.Consumer>{value => <OldComponent {...props} {...value} />}</RouterContext.Consumer>)}}export default withRouter;
实现 Prompt
src/react-router/index.js
export { default as Prompt } from './Prompt';
src/react-router/Prompt.js
import React from 'react';import RouterContext from './RouterContext';import Lifecycle from './Lifecycle';/**** @param {*} when 布尔值,表示要不要阻止跳转* @param {*} message 函数,表示要阻止的时候,显示什么信息*/function Prompt({when, message}){return (<RouterContext.Consumer>{value => {if (!when) return null;const block = value.history.block;return (<LifecycleonMount={self => {self.release = block(message);}}onUnmount={self => self.release()}/>)}}</RouterContext.Consumer>)}export default Prompt;
src/history/createBrowserHistory.js
function createTransitionManager(){let prompt = null;// 设置 promptfunction setPrompt(nextPrompt){prompt = nextPrompt;// 返回一个销毁函数return () => {if (prompt === nextPrompt) prompt = null;}}// 跳转拦截function confirmTransitionTo(location, action, getUserConfirmation, callback) {if (prompt !== null){let result = typeof prompt === 'function' ? prompt(location, action) : prompt;if (typeof result === 'string'){if (typeof getUserConfirmation === 'function'){getUserConfirmation(result, callback);} else {callback(true);}} else {callback(result !== false);}} else {callback(true);}}return {setPrompt,confirmTransitionTo,};}// 默认跳转拦截函数function getConfirmation(message, callback) {callback(window.confirm(message)); // eslint-disable-line no-alert}function createBrowserHistory(props={}){let {getUserConfirmation=getConfirmation} = props;function push(path, nextState){let action = 'PUSH';let location = createLocation(path, nextState, createKey(), history.location);transitionManager.confirmTransitionTo(location, action, getUserConfirmation, ok => {if (!ok) return; //如果确认拦截,直接返回,不进行跳转。...})// 跳转拦截let isBlocked = false;function block(prompt){if (prompt === void 0) prompt = false;let unblock = transitionManager.setPrompt(prompt);if (!isBlocked){checkDOMListeners(1);isBlocked = true;}// 返回一个用于清空拦截的函数return () => {if (isBlocked){isBlocked = false;checkDOMListeners(-1);}return unblock();}}const history = {block,}return history;}
实现 hooks
src/react-router/index.js
export * from './hooks'
src/react-router/hooks.js
import React, {useContext} from 'react';import RouterContext from './RouterContext';import matchPath from './matchPath';export function useParams (){let match = useContext(RouterContext).match;return match ? match.params : {};}export function useHistory (){return useContext(RouterContext).history;}export function useLocation (){return useContext(RouterContext).location;}export function useRouteMatch (path){let location = useLocation();let match = useContext(RouterContext).match;return path ? matchPath(location.pathname, path) : match;}
实现 lazy
import React, {Suspense} from 'react';import ReactDOM from 'react-dom';import { BrowserRouter as Router, Route, Switch} from 'react-router-dom';function lazy(load){return class extends React.Component {state = {Com: null}componentDidMount(){load().then(res => {this.setState({Com: res.default || res})})}render(){let {Com} = this.state;return Com ? <Com {...this.props} /> : null;}}}// /* webpackChunkName: "doc-[request]" */ 就是 MagicComment(魔法注释)。// 没加,home路由打包出来就是 0.chunk.js;// 加上后,打包出来文件就是 home.chunk.jsconst LazyHome = lazy(() => import(/* webpackChunkName: 'home' */'./components/Home'));const LazyProfile = lazy(() => import('./components/Profile'));function Loading(){return <div>loading...</div>;}ReactDOM.render(<Router basename="/m"><Switch><Suspense fallback={<Loading />}><Route exact path="/" component={LazyHome} /><Route path="/profile" component={LazyProfile} /></Suspense></Switch></Router>,document.getElementById('root'));
