打开 react-router-dom 源码会发现 它自身并没有很多东西,大部分都来自 react-router 和 history 库。

  • react-router-dom 放跟 dom 有关的
  • react-router 放跟 dom 无关的
  1. import { match } from 'react-router';
  2. import * as React from 'react';
  3. import * as H from 'history';

我们这里直接在 src 下创建3个文件夹 react-router-dom、react-router、history

实现 基本路由

image.png

src/index.js

  1. import React from 'react';
  2. import ReactDOM from 'react-dom';
  3. import { HashRouter as Router, Route} from './react-router-dom';
  4. import Home from './components/Home'
  5. import User from './components/User'
  6. import Profile from './components/Profile'
  7. ReactDOM.render(
  8. <Router>
  9. <Route exact path="/" component={Home} />
  10. <Route path="/user" component={User} />
  11. <Route path="/profile" component={Profile} />
  12. </Router>,
  13. document.getElementById('root')
  14. );

src/react-router-dom

src/react-router-dom/index.js
  1. export * from '../react-router';
  2. export { default as HashRouter } from './HashRouter'
  3. export { default as BrowserRouter } from './BrowserRouter'

src/react-router-dom/HashRouter.js
  1. import React from 'react';
  2. import { Router } from '../react-router';
  3. // 创建一个 hash history 对象的方法,模拟一个自己的 history 对象,但是是用 hash 实现的
  4. import { createHashHistory } from 'history';
  5. class HashRouter extends React.Component {
  6. history = createHashHistory(); // hash实现
  7. render(){
  8. return (
  9. <Router history={this.history}>
  10. {this.props.children}
  11. </Router>
  12. )
  13. }
  14. }
  15. export default HashRouter;

src/react-router-dom/BrowserRouter.js
  1. import React from 'react';
  2. import { Router } from '../react-router';
  3. import { createBrowserHistory } from 'history';
  4. class BrowserRouter extends React.Component {
  5. // 创建相应的历史对象 history,并且传入Router组件,原样渲染子组件
  6. // 不管是哪种创建历史对象的方法,得到的history 长的都一样,都像window.history
  7. history = createBrowserHistory(this.props); //window.history
  8. render(){
  9. return (
  10. <Router history={this.history}>
  11. {this.props.children}
  12. </Router>
  13. )
  14. }
  15. }
  16. export default BrowserRouter;

src/react-router

src/react-router/index.js
  1. export { default as Router } from './Router';
  2. export { default as Route } from './Route';
  3. export { default as __RouterContext } from './RouterContext';

src/react-router/RouterContext.js
  1. import React from 'react';
  2. const RouterContext = React.createContext();
  3. export default RouterContext;

src/react-router/Router.js
  1. import React from 'react';
  2. import RouterContext from './RouterContext';
  3. class Router extends React.Component {
  4. constructor(props){
  5. super(props);
  6. this.state = {
  7. location: props.history.location,
  8. }
  9. // 监听历史对象中的路径变化,当路径发生变化后执行回调函数,参数就是最新的路径对象
  10. // history.listen 函数会返回一个 取消监听 的函数,我们可以在组件卸载的时候,调用它
  11. this.unlisten = props.history.listen(location => {
  12. this.setState({location})
  13. });
  14. }
  15. componentWillUnmount(){
  16. this.unlisten && this.unlisten();
  17. }
  18. render(){
  19. // 通过value向下层传递数据
  20. let value = {
  21. location: this.state.location, //用于传递给 Route ,来判断路由是否匹配
  22. history: this.props.history, //HashRouter、BrowserHistory 组件给的
  23. }
  24. return (
  25. <RouterContext.Provider value={value}>
  26. {this.props.children}
  27. </RouterContext.Provider>
  28. )
  29. }
  30. }
  31. export default Router;

src/react-router/Route.js
  1. import React from 'react';
  2. import RouterContext from './RouterContext';
  3. /**
  4. * 1.获取 context 的值
  5. * 2.匹配路由规则里的 path 是否和当前地址中的 url 地址是否相等,
  6. * 如果相等,就渲染 component;如果不相等,就不渲染任何东西。
  7. */
  8. class Route extends React.Component {
  9. static contextType = RouterContext;
  10. render(){
  11. const {context, props} = this;
  12. const {history, location} = context;
  13. const {exact, path, component: RouteComponent, render} = props; //路由挂载属性
  14. const match = location.pathname === path; //路由规则是否匹配(match对象后边再实现)
  15. const routerProps = {history, location}; //路由属性 history、location、match
  16. let element = null;
  17. // 优先级 RouteComponent > render(匹配才渲染) > children(匹不匹配都渲染)
  18. if (match){
  19. routerProps.match = match;
  20. if (RouteComponent) {
  21. element = <RouteComponent {...routerProps} />;
  22. } else if (render){
  23. element = render(routerProps);
  24. } else if (children){
  25. element = children(routerProps);
  26. }
  27. } else {
  28. if (children) element = children(routerProps);
  29. }
  30. return element;
  31. }
  32. }
  33. export default Route;

实现 history

src/history/index.js

  1. export {default as createHashHistory} from './createHashHistory';
  2. export {default as createBrowserHistory} from './createBrowserHistory';

src/history/createHashHistory.js

未处理完

  1. function parsePath(str=window.location.hash){
  2. str = str || '#/';
  3. let hash = '', pathname = '', search = '';
  4. let searchIndex = str.indexOf('?');
  5. if (searchIndex > -1){
  6. search = str.slice(searchIndex);
  7. pathname = str.slice(1, searchIndex);
  8. } else {
  9. pathname = str.slice(1);
  10. }
  11. return {hash, pathname, search};
  12. }
  13. function stringifyPath(params={}){
  14. let {pathname, search} = params;
  15. let result = pathname;
  16. if (search){
  17. result += `${search.charAt(0) === '?' ? '' : '?'}${search}`
  18. }
  19. return result;
  20. }
  21. /**
  22. * 1.state 的处理 自己维护state
  23. * 2.历史栈的维护 自己维护一个栈
  24. * 3.浏览器自身的前进后退按钮暂时未考虑
  25. */
  26. function createHashHistory(){
  27. let stack = []; //历史栈
  28. let stackIndex = 0; //栈指针
  29. let action; //当前最后一个动作是什么动作,push 'PUSH'; go 'POP';
  30. let state; //状态
  31. let listeners = []; //存放 监听函数 的数组,在路由变化后,遍历里面的每一个监听函数
  32. let location = parsePath();
  33. location.state = state;
  34. window.location.hash = window.location.hash ? window.location.hash.slice(1) : '/';;
  35. // 参数 listener 是监听函数
  36. function listen(listener){
  37. listeners.push(listener);
  38. return () => { // 返回一个销毁函数,用于取消监听
  39. listeners = listeners.filter(item => item !== listener)
  40. }
  41. }
  42. // 监听页面路由变化
  43. setTimeout(() => {
  44. window.addEventListener('hashchange', () => {
  45. let location = parsePath();
  46. Object.assign(history, {action, location: {...location, state}});
  47. if (action === 'PUSH'){
  48. stack[++stackIndex] = history.location;
  49. } else if (action === 'REPLACE'){
  50. stack[stackIndex] = history.location;
  51. }
  52. // debugger;
  53. // action = '';
  54. listeners.forEach(item => item(history.location));
  55. })
  56. })
  57. function push(pathname, nextState){
  58. action = 'PUSH';
  59. if (typeof pathname === 'object'){
  60. state = pathname.state;
  61. pathname = stringifyPath(pathname);
  62. } else {
  63. state = nextState;
  64. }
  65. window.location.hash = pathname;
  66. }
  67. function replace(pathname, nextState){
  68. action = 'REPLACE';
  69. if (typeof pathname === 'object'){
  70. state = pathname.state;
  71. pathname = stringifyPath(pathname);
  72. } else {
  73. state = nextState;
  74. }
  75. window.location.hash = pathname;
  76. }
  77. function go(n){
  78. action = 'POP';
  79. stackIndex += n;
  80. let nextLocation = stack[stackIndex];
  81. state = nextLocation.state;
  82. window.location.hash = stringifyPath(nextLocation);
  83. }
  84. function goBack(){
  85. go(-1);
  86. }
  87. function goForward(){
  88. go(1);
  89. }
  90. const history = {
  91. action: 'POP',
  92. // block,
  93. // createHref,
  94. go,
  95. goBack,
  96. goForward,
  97. // length,
  98. listen,
  99. location,
  100. push,
  101. replace,
  102. }
  103. stack[stackIndex] = history.location;
  104. return history;
  105. }
  106. export default createHashHistory;

src/history/createBrowserHistory

  1. import resolvePathname from 'resolve-pathname';
  2. const PopStateEvent = 'popstate'; //h5 history API中的监听事件
  3. // const HashChangeEvent = 'hashchange'; //监听hash变化
  4. // 添加首位斜杠
  5. function addLeadingSlash(path){
  6. return path.charAt(0) === '/' ? path : `/${path}`;
  7. }
  8. // 移除首位斜杠
  9. // function stripLeadingSlash(path) {
  10. // return path.charAt(0) === '/' ? path.substr(1) : path;
  11. // }
  12. // 移除末尾斜杠
  13. function stripTrailingSlash(path) {
  14. return path.charAt(path.length - 1) === '/' ? path.slice(0, -1) : path;
  15. }
  16. // 检测是否有basename
  17. function hasBasename(path, prefix) {
  18. return path.toLowerCase().indexOf(prefix.toLowerCase()) === 0 && '/?#'.indexOf(path.charAt(prefix.length)) !== -1;
  19. }
  20. // 移除basename
  21. function stripBasename(path, prefix) {
  22. return hasBasename(path, prefix) ? path.substr(prefix.length) : path;
  23. }
  24. function getHistoryState(){
  25. try {
  26. return window.history.state || {};
  27. } catch (e) {
  28. // IE 11 sometimes throws when accessing window.history.state
  29. // See https://github.com/ReactTraining/history/pull/289
  30. return {};
  31. }
  32. }
  33. // 解析 path 路径
  34. function parsePath(path){
  35. let pathname = path || '/';
  36. let search = '';
  37. let hash = '';
  38. let hashIndex = pathname.indexOf('#');
  39. if (hashIndex !== -1){
  40. hash = pathname.slice(hashIndex);
  41. pathname = pathname.slice(0, hashIndex);
  42. hash = hash === '#' ? '' : hash;
  43. }
  44. let searchIndex = pathname.indexOf('?');
  45. if (searchIndex !== -1){
  46. search = pathname.slice(searchIndex);
  47. pathname = pathname.slice(0, searchIndex);
  48. search = search === '?' ? '' : search;
  49. }
  50. return {pathname, search, hash};
  51. }
  52. // 根据 location 对象,创建 path 路径
  53. function createPath(location={}){
  54. let {pathname, search, hash} = location;
  55. let path = pathname || '/';
  56. if (search && search !== '?') path += (search.charAt(0) === '?' ? search : `?${search}`);
  57. if (hash && hash !== '#') path += (hash.charAt(0) === '#' ? hash : `#${hash}`);
  58. return path;
  59. }
  60. /** 创建新的 location 对象
  61. *
  62. * @param {*} path 路径 string | object
  63. * @param {*} state state数据
  64. * @param {*} key key唯一标识符
  65. * @param {*} currentLocation 当前的 location 对象
  66. * @returns
  67. */
  68. function createLocation(path, state, key, currentLocation){
  69. let location;
  70. if (typeof path === 'string'){
  71. location = parsePath(path);
  72. location.state = state;
  73. } else {
  74. location = Object.assign({}, path);
  75. if (location.pathname === undefined) location.pathname = '';
  76. if (location.search){
  77. if (location.search.charAt(0) !== '?') location.search = `?${location.search}`;
  78. } else {
  79. location.search = '';
  80. }
  81. if (location.hash){
  82. if (location.hash.charAt(0) !== '#') location.hash = `#${location.hash}`;
  83. } else {
  84. location.hash = '';
  85. }
  86. if (state !== undefined && location.state === undefined) location.state = state;
  87. }
  88. try {
  89. location.pathname = decodeURI(location.pathname);
  90. } catch(e){
  91. throw e;
  92. }
  93. if (key) location.key = key;
  94. if (currentLocation){
  95. if (!location.pathname){
  96. location.pathname = currentLocation.pathname;
  97. } else if(location.pathname.charAt(0) !== '/'){
  98. location.pathname = resolvePathname(location.pathname, currentLocation.pathname);
  99. }
  100. } else {
  101. if (!location.pathname) location.pathname = '/';
  102. }
  103. return location;
  104. }
  105. function createTransitionManager(){
  106. let prompt = null;
  107. // 设置 prompt
  108. function setPrompt(nextPrompt){
  109. prompt = nextPrompt;
  110. // 返回一个销毁函数
  111. return () => {
  112. if (prompt === nextPrompt) prompt = null;
  113. }
  114. }
  115. // 跳转拦截
  116. function confirmTransitionTo(location, action, getUserConfirmation, callback) {
  117. if (prompt !== null){
  118. let result = typeof prompt === 'function' ? prompt(location, action) : prompt;
  119. if (typeof result === 'string'){
  120. if (typeof getUserConfirmation === 'function'){
  121. getUserConfirmation(result, callback);
  122. } else {
  123. callback(true);
  124. }
  125. } else {
  126. callback(result !== false);
  127. }
  128. } else {
  129. callback(true);
  130. }
  131. }
  132. // 添加监听
  133. let listeners = []; //存放 监听函数 的数组,在路由变化后,遍历里面的每一个监听函数
  134. function appendListener(fn){
  135. let isActive = true;
  136. function listener(...args){
  137. if (isActive) fn.apply(void 0, args);
  138. }
  139. listeners.push(listener);
  140. // 返回一个销毁函数,用于取消监听
  141. return () => {
  142. isActive = false;
  143. listeners = listeners.filter(item => item !== listener);
  144. }
  145. }
  146. // 触发监听
  147. function notifyListeners(...args){
  148. listeners.forEach(listener => listener.apply(void 0, args));
  149. }
  150. return {
  151. setPrompt,
  152. confirmTransitionTo,
  153. appendListener,
  154. notifyListeners,
  155. };
  156. }
  157. // 默认跳转拦截函数
  158. function getConfirmation(message, callback) {
  159. callback(window.confirm(message)); // eslint-disable-line no-alert
  160. }
  161. function isExtraneousPopstateEvent(event) {
  162. return event.state === undefined && navigator.userAgent.indexOf('CriOS') === -1;
  163. }
  164. /** 创建 browser history 对象
  165. * Creates a history object that uses the HTML5 history API including
  166. * pushState, replaceState, and the popstate event.
  167. *
  168. * @param {*} props.basename 基链接
  169. * @param {*} props.keylength key长度
  170. * @param {*} props.forceRefresh 是否强制刷新
  171. * @param {*} props.getUserConfirmation 自定义跳转拦截函数
  172. * @returns history 对象
  173. */
  174. function createBrowserHistory(props={}){
  175. const globalHistory = window.history; //h5 history 全局对象
  176. let {keyLength=6, forceRefresh, getUserConfirmation=getConfirmation} = props;
  177. let basename = props.basename ? stripTrailingSlash(addLeadingSlash(props.basename)) : '';
  178. // 根据 window.location 创建 location 对象
  179. function getDOMLocation(historyState){
  180. let ref = historyState || {};
  181. let {key, state} = ref;
  182. let _location = window.location;
  183. let {pathname, search, hash} = _location;
  184. let path = pathname + search + hash;
  185. if (basename) path = stripBasename(path, basename);
  186. return createLocation(path, state, key);
  187. }
  188. // 创建key
  189. function createKey() {
  190. return Math.random().toString(36).substr(2, keyLength);
  191. }
  192. let transitionManager = createTransitionManager();
  193. // 更新 history,并触发一次监听函数
  194. function setState(nextState){
  195. Object.assign(history, nextState);
  196. history.length = globalHistory.length;
  197. transitionManager.notifyListeners(history.location, history.action);
  198. }
  199. let initialLocation = getDOMLocation(getHistoryState());
  200. let allKeys = [initialLocation.key]; // 存储 历史页面的key
  201. function createHref(location){
  202. return `${basename}${createPath(location)}`;
  203. }
  204. function push(path, nextState){
  205. let action = 'PUSH';
  206. let location = createLocation(path, nextState, createKey(), history.location);
  207. transitionManager.confirmTransitionTo(location, action, getUserConfirmation, ok => {
  208. if (!ok) return; //如果确认拦截,直接返回,不进行跳转。
  209. let href = createHref(location);
  210. let {key, state} = location;
  211. globalHistory.pushState({key, state}, null, href);
  212. if (forceRefresh){
  213. window.location.href = href;
  214. } else {
  215. let prevIndex = allKeys.indexOf(history.location.key);
  216. let nextKeys = allKeys.slice(0, prevIndex + 1);
  217. nextKeys.push(key);
  218. allKeys = nextKeys;
  219. setState({action, location});
  220. }
  221. })
  222. }
  223. function replace(path, nextState){
  224. let action = 'REPLACE';
  225. let location = createLocation(path, nextState, createKey(), history.location);
  226. transitionManager.confirmTransitionTo(location, action, getUserConfirmation, ok => {
  227. if (!ok) return; //如果确认拦截,直接返回,不进行跳转。
  228. let href = createHref(location);
  229. let {key, state} = location;
  230. globalHistory.replaceState({key, state}, null, href);
  231. if (forceRefresh){
  232. window.location.replace(href);
  233. } else {
  234. let prevIndex = allKeys.indexOf(history.location.key);
  235. if (prevIndex !== -1) allKeys[prevIndex] = key;
  236. setState({action, location});
  237. }
  238. })
  239. }
  240. function go(n){
  241. globalHistory.go(n);
  242. }
  243. function goBack(){
  244. go(-1);
  245. }
  246. function goForward(){
  247. go(1);
  248. }
  249. function handlePopState(event){
  250. if (isExtraneousPopstateEvent(event)) return;
  251. handlePop(getDOMLocation(event.state));
  252. }
  253. let forceNextPop = false;
  254. function handlePop(location){
  255. if (forceNextPop){
  256. forceNextPop = false;
  257. setState();
  258. } else {
  259. let action = 'POP';
  260. transitionManager.confirmTransitionTo(location, action, getUserConfirmation, ok => {
  261. if (ok){
  262. setState({action, location});
  263. } else {
  264. revertPop(location);
  265. }
  266. })
  267. }
  268. }
  269. //如果确认拦截,要回退回原路由
  270. function revertPop(fromLocation){
  271. let toLocation = history.location;
  272. let toIndex = allKeys.indexOf(toLocation.key);
  273. if (toIndex === -1) toIndex = 0;
  274. let fromIndex = allKeys.indexOf(fromLocation.key);
  275. if (fromIndex === -1) fromIndex = 0;
  276. let delta = toIndex - fromIndex; //如果不为0,说明发生了前进或后退
  277. if (delta){
  278. forceNextPop = true;
  279. go(delta);
  280. }
  281. }
  282. // 监听事件的添加与移除
  283. let listenerCount = 0; //已绑定的监听函数数量
  284. function checkDOMListeners(delta){
  285. listenerCount += delta;
  286. // 当回退或前进的时候,会执行 popstate 事件。(popstate事件是浏览器自带的,默认支持)
  287. if (listenerCount === 1 && delta === 1){
  288. window.addEventListener(PopStateEvent, handlePopState);
  289. } else if (listenerCount === 0){
  290. window.removeEventListener(PopStateEvent, handlePopState);
  291. }
  292. }
  293. function listen(listener){
  294. let unlisten = transitionManager.appendListener(listener);
  295. checkDOMListeners(1);
  296. return () => {
  297. checkDOMListeners(-1);
  298. unlisten();
  299. }
  300. }
  301. // 跳转拦截
  302. let isBlocked = false; //是否阻止跳转
  303. function block(prompt){
  304. if (prompt === void 0) prompt = false;
  305. let unblock = transitionManager.setPrompt(prompt);
  306. if (!isBlocked){
  307. checkDOMListeners(1);
  308. isBlocked = true;
  309. }
  310. // 返回一个用于清空拦截的函数
  311. return () => {
  312. if (isBlocked){
  313. isBlocked = false;
  314. checkDOMListeners(-1);
  315. }
  316. return unblock();
  317. }
  318. }
  319. const history = {
  320. length: globalHistory.length,
  321. action: 'POP',
  322. location: initialLocation,
  323. createHref,
  324. push,
  325. replace,
  326. go,
  327. goBack,
  328. goForward,
  329. block,
  330. listen,
  331. }
  332. return history;
  333. }
  334. export default createBrowserHistory;

path-to-regexp 介绍

/home结束
  1. let pathToRegExp = require('path-to-regexp');
  2. let regExp = pathToRegExp('/home', [], {end:true});
  3. console.log(regExp); // /^\/home\/?$/i
  4. console.log(regExp.test('/home')); //true
  5. console.log(regExp.test('/home/2')); //false

image.png

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

image.png

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

image.png

正则匹配

更多正则知识见 js-正则篇:https://www.yuque.com/zhuchaoyang/wrif6k/grhxy3

  • 是否捕获 | 表达式 | 含义 | | —- | —- | | () | 表示捕获分组,()会把每个分组里的匹配的值保存起来,使用$n(n是一个数字,表示第n个捕获组的内容) | | (?:) | 表示非捕获分组,和捕获分组唯一的区别在于,非捕获分组匹配的值不会保存起来 | | (?…) | 表示命名捕获分组,反向引用一个命名分组的语法是 \k ,在 replace() 方法的替换字符串中反向引用是用 $ |

  • 前瞻和后顾 | exp(?=pattern) | 正向肯定查找(前瞻),后面必须跟着什么 | | —- | —- | | exp(?!pattern) | 正向否定查找(前瞻),后面不能跟着什么 | | exp(?<=pattern) | 反向肯定条件查找(后顾),不捕获 | | exp(?<!pattern) | 反向否定条件查找(后顾) |

  1. //会消耗掉字符的
  2. //console.log('1a'.match(/\d[a-z][a-z]/));
  3. //?= 正向肯定查找 不消费字符 正向前瞻
  4. //console.log('1a'.match(/\d(?=[a-z])[a-z]/));
  5. //匹配分组捕获
  6. console.log('1ab'.match(/1([a-z])([a-z])/)); //[ '1ab', 'a', 'b' ]
  7. //非捕获分组
  8. console.log('1ab'.match(/1(?:[a-z])([a-z])/)); //[ '1ab', 'b' ]
  9. //正向肯定前瞻 (?=[a-z]) 只是一个判断条件,只匹配右边(后边)紧跟小写字母的数字
  10. console.log('1a'.match(/\d(?=[a-z])[a-z]/)); //[ '1a' ]
  11. //正向否定前瞻 (?![A-z]) 只匹配右边紧跟不是大写字母的数字
  12. console.log('1a'.match(/\d(?![A-Z])[a-z]/)); //[ '1a' ]
  13. //反向肯定前瞻 (?<=[a-z]) 只匹配左边(前边)是小写字母的数字
  14. console.log('1a'.match(/(?<=[a-z])\d[a-z]/)); //null
  15. console.log('b1a'.match(/(?<=[a-z])\d[a-z]/)); //[ '1a' ]
  16. //反向否定前瞻 (?<![A-Z]) 只匹配左边不是大写字母的数字
  17. console.log('1a'.match(/(?<![A-Z])\d[a-z]/)); //[ '1a' ]
  18. 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(/(?\d{2})-\k/)); // \k 表示 引用上一个分组的内容,即 11 /* [ ‘11-11’, ‘11’, index: 0, input: ‘11-11’, groups: [Object: null prototype] { x: ‘11’ } ] /

console.log(‘11-22’.replace(/(?\d{2})-(?\d{2})/, “$-$“)); //22-11 // $ 表示 引用 y分组 的内容

  1. <a name="jLKTG"></a>
  2. # 实现 matchPath
  3. <a name="O8o15"></a>
  4. ## src/react-router/index.js
  5. ```javascript
  6. export { default as Router } from './Router';
  7. export { default as Route } from './Route';
  8. export { default as __RouterContext } from './RouterContext';
  9. + export { default as matchPath } from './matchPath';

src/react-router/Router.js

  1. import React from 'react';
  2. import RouterContext from './RouterContext';
  3. class Router extends React.Component {
  4. + static computeRootMatch(pathname){
  5. + return { path: '/', url: '/', params: {}, isExact: pathname === '/' }
  6. + }
  7. constructor(props){
  8. super(props);
  9. this.state = {
  10. location: props.history.location,
  11. }
  12. // 监听历史对象中的路径变化,当路径发生变化后执行回调函数,参数就是最新的路径对象
  13. // history.listen 函数会返回一个 取消监听 的函数,我们可以在组件卸载的时候,调用它
  14. this.unlisten = props.history.listen(location => {
  15. this.setState({location})
  16. });
  17. }
  18. componentWillUnmount(){
  19. this.unlisten && this.unlisten();
  20. }
  21. render(){
  22. //通过value向下层传递数据
  23. let value = {
  24. location: this.state.location, //用于传递给 Route ,来判断路由是否匹配
  25. history: this.props.history, //HashRouter、BrowserHistory 组件给的
  26. + match: Router.computeRootMatch(this.state.location.pathname), //根 match
  27. }
  28. return (
  29. <RouterContext.Provider value={value}>
  30. {this.props.children}
  31. </RouterContext.Provider>
  32. )
  33. }
  34. }
  35. export default Router;

src/react-router/Route.js

  1. import React from 'react';
  2. import RouterContext from './RouterContext';
  3. + import matchPath from './matchPath';
  4. class Route extends React.Component {
  5. static contextType = RouterContext;
  6. render(){
  7. const {context, props} = this;
  8. const {history, location} = context;
  9. + const {component: RouteComponent, render} = props; //路由挂载属性
  10. + const match = matchPath(location.pathname, props); //路由匹配
  11. const routerProps = {history, location}; //路由属性 history、location、match
  12. let element = null;
  13. // 优先级 RouteComponent > render(匹配才渲染) > children(匹不匹配都渲染)
  14. if (match){
  15. routerProps.match = match;
  16. if (RouteComponent) {
  17. element = <RouteComponent {...routerProps} />;
  18. } else if (render){
  19. element = render(routerProps);
  20. } else if (children){
  21. element = children(routerProps);
  22. }
  23. } else {
  24. if (children) element = children(routerProps);
  25. }
  26. // 重写Provide的value值
  27. + return (
  28. + <RouterContext.Provider value={routerProps}>
  29. + {element}
  30. + </RouterContext.Provider>
  31. + );
  32. }
  33. }
  34. export default Route;

src/react-router/matchPath.js

  1. const pathToRegExp = require('path-to-regexp');
  2. const cache = {}; //正则匹配路径缓存
  3. // 获取路径匹配正则表达式
  4. function compilePath(path, options){
  5. const cacheKey = `${path}${JSON.stringify(options)}`;
  6. if (cache[cacheKey]) return cache[cacheKey];
  7. const keys = [];
  8. const regExp = pathToRegExp(path, keys, options);
  9. let result = {keys, regExp};
  10. cache[cacheKey] = result;
  11. return result;
  12. }
  13. /** 路由匹配方法
  14. *
  15. * @param {*} pathname 浏览器中当前的真实的路径名
  16. * @param {*} options 其实就是 Route组件 的属性
  17. * path Route组件传递的原路径
  18. * exact 是否精确匹配(是否匹配整个字符串,后面不能跟子路径) /home /home/aa
  19. * strict 是否严格匹配(是否允许结尾有一个可选的/ ) /home/ /home
  20. * sensitive 是否大小写敏感 /home /HOME
  21. */
  22. function matchPath(pathname, options={}){
  23. const {path="/", exact=false, strict=false, sensitive=false} = options;
  24. const {keys, regExp} = compilePath(path, {end: exact, strict, sensitive});
  25. const match = regExp.exec(pathname);
  26. // 如果不匹配,返回 null
  27. if (!match) return null;
  28. const [url, ...values] = match;
  29. const isExact = pathname === url;
  30. // 如果要求精确,但是不精确,也返回 null
  31. if (exact && !isExact) return null;
  32. const params = keys.reduce((obj, key, index) => {
  33. obj[key.name] = values[index];
  34. return obj;
  35. }, {})
  36. return {isExact, params, path, url};
  37. }
  38. export default matchPath;

实现 Switch

src/index.js demo测试
  1. import React from 'react';
  2. import ReactDOM from 'react-dom';
  3. import { BrowserRouter as Router, Route, Switch} from './react-router-dom';
  4. ReactDOM.render(
  5. <Router basename="/m">
  6. <Switch>
  7. <Route exact path="/" component={Home} />
  8. <Route path="/about" component={<div>about</div>} />
  9. <Route path="/:user" render={props => <div>页面:{props.match.params.user}</div>} />
  10. <Route render={props => <div>未发现路由页面</div>} />
  11. </Switch>
  12. </Router>,
  13. document.getElementById('root')
  14. );

src/react-router/index.js
  1. + export { default as Switch } from './Switch';

src/react-router/Route.js
  1. class Route extends React.Component {
  2. static contextType = RouterContext;
  3. render(){
  4. let element = null;
  5. const {context, props: ps} = this;
  6. const {history, location} = context;
  7. + const {component: RouteComponent, render, computedMatch} = ps; //路由挂载属性
  8. + const match = computedMatch ? computedMatch : matchPath(location.pathname, ps); //路由匹配
  9. const routerProps = {history, location, match};
  10. ...
  11. }
  12. }
  13. export default Route;

src/react-router/Switch.js
  1. import React from 'react';
  2. import RouterContext from './RouterContext'
  3. import matchPath from './matchPath'
  4. class Switch extends React.Component {
  5. static contextType = RouterContext;
  6. render (){
  7. const {context, props: ps} = this;
  8. const {location} = context;
  9. let element, match;
  10. // 因为 this.props.children 可以是 undefined,可以是对象,可以是数组,也可能是字符串或数字;
  11. // 所以不能直接用 this.props.children.forEach,React.children.forEach 帮我们做了兼容性处理
  12. React.Children.forEach(ps.children, child => {
  13. // 只要有一个率先匹配上,后边的路由就不再走了。
  14. if (!match && React.isValidElement(child)){
  15. element = child;
  16. match = matchPath(location.pathname, child.props);
  17. }
  18. })
  19. return match ? React.cloneElement(element, {computedMatch: match}) : null;
  20. }
  21. }
  22. export default Switch;

实现 Redirect

src/react-router.js
  1. export { default as Redirect } from './Redirect';

src/react-router/ Redirect.js 简易版
  1. import React from 'react';
  2. import Lifecycle from './Lifecycle';
  3. import RouterContext from './RouterContext';
  4. function Redirect({from, to, exact=false, strict=false}){
  5. return (
  6. <RouterContext.Consumer>
  7. {({history}) => {
  8. return (<Lifecycle onMount={() => history.push(to)} />);
  9. }}
  10. </RouterContext.Consumer>
  11. )
  12. }
  13. export default Redirect;

src/react-router/Lifecycle.js 创建一个生命周期类,不返回东西,只处理逻辑
  1. import React from 'react';
  2. class Lifecycle extends React.Component {
  3. componentDidMount(){
  4. this.props.onMount && this.props.onMount(this);
  5. }
  6. componentWillUnmount(){
  7. this.props.onUnmount && this.props.onUnmount(this);
  8. }
  9. render (){
  10. return null;
  11. }
  12. }
  13. export default Lifecycle;

实现 Link

src/react-router-dom/index.js
  1. export { default as Link } from './Link'

src/react-router-dom/Link.js
  1. import React from 'react';
  2. import { __RouterContext as RouterContext } from '../react-router';
  3. function Link (props){
  4. return (
  5. <RouterContext.Consumer>
  6. {({history}) => (
  7. <a {...props} href="" onClick={(event) => {
  8. event.preventDefault(); //阻止 a标签 的默认事件
  9. history.push(props.to);
  10. }}>{props.children}</a>
  11. )}
  12. </RouterContext.Consumer>
  13. )
  14. }
  15. export default Link;

实现 NavLink

demo: https://www.yuque.com/zhuchaoyang/cx9erp/gm2lb9#ehYUV

src/react-router-dom/index.js
  1. export { default as NavLink } from './NavLink'

src/react-router-dom/NavLink.js

源码的实现方式

  1. import React from 'react';
  2. import { __RouterContext as RouterContext, matchPath } from '../react-router';
  3. import { Link } from './'
  4. function NavLink (props){
  5. let context = React.useContext(RouterContext);
  6. const {
  7. to, //匹配的路径
  8. className: classNameProp = '', //原生类名
  9. activeClassName='', //激活类名
  10. style: styleProp = {}, //原始样式
  11. activeStyle={}, //激活样式
  12. children,
  13. exact,
  14. strict,
  15. } = props;
  16. // 当前的路径pathname 和 to路径是否匹配
  17. const path = typeof to === 'object' ? to.pathname : to;
  18. let isActive = matchPath(context.location.pathname, {path, exact, strict});
  19. let className = isActive ? joinClassName(classNameProp, activeClassName) : classNameProp;
  20. let style = isActive ? {...styleProp, ...activeStyle} : styleProp;
  21. let linkProps = { className, style, to, children };
  22. return <Link {...linkProps} />
  23. }
  24. function joinClassName(...classNames){
  25. return classNames.filter(c => c).join(' '); //过滤掉空字符串
  26. }
  27. export default NavLink;

利用 Route 组件的 children 实现

  1. import React from 'react';
  2. import { Route } from '../react-router';
  3. import { Link } from './'
  4. function NavLink(props){
  5. const {
  6. to, //匹配的路径
  7. className: classNameProp = '', //原生类名
  8. activeClassName='', //激活类名
  9. style: styleProp = {}, //原始样式
  10. activeStyle={}, //激活样式
  11. children,
  12. exact,
  13. strict,
  14. } = props;
  15. return (
  16. <Route
  17. path={typeof to === 'object' ? to.pathname : to}
  18. exact={exact}
  19. strict={strict}
  20. children={({match: isActive}) => {
  21. let className = isActive ? joinClassName(classNameProp, activeClassName) : classNameProp;
  22. let style = isActive ? {...styleProp, ...activeStyle} : styleProp;
  23. let linkProps = { className, style, to, children };
  24. return <Link {...linkProps} />
  25. }} />
  26. )
  27. }
  28. export default NavLink;

实现 withRouter

src/react-router/index.js
  1. export { default as withRouter } from './withRouter';

src/react-router/withRouter.js
  1. import React from 'react';
  2. import RouterContext from './RouterContext';
  3. function withRouter(OldComponent){
  4. return props => {
  5. return (
  6. <RouterContext.Consumer>
  7. {value => <OldComponent {...props} {...value} />}
  8. </RouterContext.Consumer>
  9. )
  10. }
  11. }
  12. export default withRouter;

实现 Prompt

src/react-router/index.js
  1. export { default as Prompt } from './Prompt';

src/react-router/Prompt.js
  1. import React from 'react';
  2. import RouterContext from './RouterContext';
  3. import Lifecycle from './Lifecycle';
  4. /**
  5. *
  6. * @param {*} when 布尔值,表示要不要阻止跳转
  7. * @param {*} message 函数,表示要阻止的时候,显示什么信息
  8. */
  9. function Prompt({when, message}){
  10. return (
  11. <RouterContext.Consumer>
  12. {value => {
  13. if (!when) return null;
  14. const block = value.history.block;
  15. return (
  16. <Lifecycle
  17. onMount={self => {
  18. self.release = block(message);
  19. }}
  20. onUnmount={self => self.release()}
  21. />
  22. )
  23. }}
  24. </RouterContext.Consumer>
  25. )
  26. }
  27. export default Prompt;

src/history/createBrowserHistory.js
  1. function createTransitionManager(){
  2. let prompt = null;
  3. // 设置 prompt
  4. function setPrompt(nextPrompt){
  5. prompt = nextPrompt;
  6. // 返回一个销毁函数
  7. return () => {
  8. if (prompt === nextPrompt) prompt = null;
  9. }
  10. }
  11. // 跳转拦截
  12. function confirmTransitionTo(location, action, getUserConfirmation, callback) {
  13. if (prompt !== null){
  14. let result = typeof prompt === 'function' ? prompt(location, action) : prompt;
  15. if (typeof result === 'string'){
  16. if (typeof getUserConfirmation === 'function'){
  17. getUserConfirmation(result, callback);
  18. } else {
  19. callback(true);
  20. }
  21. } else {
  22. callback(result !== false);
  23. }
  24. } else {
  25. callback(true);
  26. }
  27. }
  28. return {
  29. setPrompt,
  30. confirmTransitionTo,
  31. };
  32. }
  33. // 默认跳转拦截函数
  34. function getConfirmation(message, callback) {
  35. callback(window.confirm(message)); // eslint-disable-line no-alert
  36. }
  37. function createBrowserHistory(props={}){
  38. let {getUserConfirmation=getConfirmation} = props;
  39. function push(path, nextState){
  40. let action = 'PUSH';
  41. let location = createLocation(path, nextState, createKey(), history.location);
  42. transitionManager.confirmTransitionTo(location, action, getUserConfirmation, ok => {
  43. if (!ok) return; //如果确认拦截,直接返回,不进行跳转。
  44. ...
  45. })
  46. // 跳转拦截
  47. let isBlocked = false;
  48. function block(prompt){
  49. if (prompt === void 0) prompt = false;
  50. let unblock = transitionManager.setPrompt(prompt);
  51. if (!isBlocked){
  52. checkDOMListeners(1);
  53. isBlocked = true;
  54. }
  55. // 返回一个用于清空拦截的函数
  56. return () => {
  57. if (isBlocked){
  58. isBlocked = false;
  59. checkDOMListeners(-1);
  60. }
  61. return unblock();
  62. }
  63. }
  64. const history = {
  65. block,
  66. }
  67. return history;
  68. }

实现 hooks

src/react-router/index.js
  1. export * from './hooks'

src/react-router/hooks.js
  1. import React, {useContext} from 'react';
  2. import RouterContext from './RouterContext';
  3. import matchPath from './matchPath';
  4. export function useParams (){
  5. let match = useContext(RouterContext).match;
  6. return match ? match.params : {};
  7. }
  8. export function useHistory (){
  9. return useContext(RouterContext).history;
  10. }
  11. export function useLocation (){
  12. return useContext(RouterContext).location;
  13. }
  14. export function useRouteMatch (path){
  15. let location = useLocation();
  16. let match = useContext(RouterContext).match;
  17. return path ? matchPath(location.pathname, path) : match;
  18. }

实现 lazy

  1. import React, {Suspense} from 'react';
  2. import ReactDOM from 'react-dom';
  3. import { BrowserRouter as Router, Route, Switch} from 'react-router-dom';
  4. function lazy(load){
  5. return class extends React.Component {
  6. state = {Com: null}
  7. componentDidMount(){
  8. load().then(res => {
  9. this.setState({Com: res.default || res})
  10. })
  11. }
  12. render(){
  13. let {Com} = this.state;
  14. return Com ? <Com {...this.props} /> : null;
  15. }
  16. }
  17. }
  18. // /* webpackChunkName: "doc-[request]" */ 就是 MagicComment(魔法注释)。
  19. // 没加,home路由打包出来就是 0.chunk.js;
  20. // 加上后,打包出来文件就是 home.chunk.js
  21. const LazyHome = lazy(() => import(/* webpackChunkName: 'home' */'./components/Home'));
  22. const LazyProfile = lazy(() => import('./components/Profile'));
  23. function Loading(){
  24. return <div>loading...</div>;
  25. }
  26. ReactDOM.render(
  27. <Router basename="/m">
  28. <Switch>
  29. <Suspense fallback={<Loading />}>
  30. <Route exact path="/" component={LazyHome} />
  31. <Route path="/profile" component={LazyProfile} />
  32. </Suspense>
  33. </Switch>
  34. </Router>,
  35. document.getElementById('root')
  36. );