打开 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.history
history = createBrowserHistory(this.props); //window.history
render(){
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、match
let 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;
}
// 检测是否有basename
function hasBasename(path, prefix) {
return path.toLowerCase().indexOf(prefix.toLowerCase()) === 0 && '/?#'.indexOf(path.charAt(prefix.length)) !== -1;
}
// 移除basename
function 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/289
return {};
}
}
// 解析 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;
// 设置 prompt
function 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);
}
// 创建key
function 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]; // 存储 历史页面的key
function 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\/?$/i
console.log(regExp.test('/home')); //true
console.log(regExp.test('/home/2')); //false
/home非结束
let pathToRegExp = require('path-to-regexp');
let regExp = pathToRegExp('/home', [], {end:false});
console.log(regExp); // /^\/home\/?(?=\/|$)/i
console.log(regExp.test('/home')); //true
console.log(regExp.test('/home/')); //true
console.log(regExp.test('/home//')); //true
console.log(regExp.test('/home/2')); //true
路径参数
let pathToRegExp = require('path-to-regexp');
let regExp = pathToRegExp('/user/:id', [], {end: false});
console.log(regExp); // /^\/user\/(?:([^\/]+?))\/?(?=\/|$)/i
console.log(regExp.test('/user/1')); //true
console.log(regExp.test('/user/1/')); //true
console.log(regExp.test('/user/1//')); //true
console.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]/)); //null
console.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
```javascript
export { 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、match
let 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);
// 如果不匹配,返回 null
if (!match) return null;
const [url, ...values] = match;
const isExact = pathname === url;
// 如果要求精确,但是不精确,也返回 null
if (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 (
<Route
path={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 (
<Lifecycle
onMount={self => {
self.release = block(message);
}}
onUnmount={self => self.release()}
/>
)
}}
</RouterContext.Consumer>
)
}
export default Prompt;
src/history/createBrowserHistory.js
function createTransitionManager(){
let prompt = null;
// 设置 prompt
function 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.js
const 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')
);