什么是前端路由

首先,路由的概念开始是后端提出来的,用来跟服务器进行数据/资源获取的一种方式,通过不同的路径,来获取不同的资源 前端随着ajax(不刷新页面情况下请求数据)的流行,推动着异步交互体验提升,随后spa<单页面应用程序>在前端领域大放异彩

spa: 单页面应用程序不仅在页面内交互是无刷新的,连页面之间的跳转也没有刷新。

spa核心思想

  • 监听url变化
  • 改变context的值
  • 匹配相对应的组件
  • 实现前端路由应该包括哪些功能

    前端可以自己维护和控制浏览器的history<历史记录>。我们称之为history栈, 保证浏览器在url改变的时候不会刷新页面。并且通过history栈控制浏览器页面的前进和后退

    目前 Router有两种实现方式 hash 和 History

    hash表示页面中的一个位置,当浏览器页面完全加载好了,页面会滚动到hash位置指定的地。

  • hash 只作用在浏览器,不会在请求中发送给服务器
  • hash 发生变化时,浏览器并不会重新给后端发送请求加载页面。
  • hash 发生变化时会触发 hashchange 事件,在该事件中可以通过 window.location.hash 获取到当前 hash值
  • History

    在 html5 中新增了 history.pushState() 和 history.replaceState(),相比hash路由 url上不美观的hash值 ,取而代之使用 history.pushState 来完成对 window.location 的操作。

    History和Hash对比

  • hash后面使用#来模拟完整的路径,不太美观
  • 用户手动刷新页面, 对于hashRouter来说,后端收到的是同一个地址, history因为直接修改浏览器url,对于后端而言接受不同的地址,需要后端对资源做统一跳转处理 webpack本地开发模式下,用webpack-dev-server插件开启本地服务器,解决请求资源: historyapifallback
  • React-Router

    React作为前端视图层的框架,本身是不具有除了view层面以外的功能。需要引入React-Router, 对 react的来说管理路由需要管理组件的生命周期,对不同的路由渲染不同的组件。

    React-Router 库包含三个包:react-router、react-router-dom 和 react-router-native 。 路由操作的核心包 react-router ,而其他两个是特定环境下使用的。如果我们开发web应用,使用react-router-dom,如果开发RN相关react-router-native。

    React-Router实现单页面应用程序路由跳转,分为HashRouter, BrowserRouter browserHistory 是使用 React-Router 的应用推荐的 history方案

    学前小知识

  • html5 history.pushstate && window.popstate
  • React.Context
  • 发布订阅设计模式
  • history.pushstate

    history.pushState方法向当前浏览器会话的历史堆栈中添加一个状态 history.pushState(state, title[, url])

  • state: state状态是一个JavaScript对象, 每当用户导航到新状态时,都会触发popstate事件,并且该事件的状态属性指向了创建历史条目的创建的state属性
  • title: 大多数浏览器都会忽略这个属性
  • url: 新历史记录条目的URL由此参数指定, "新的url必须和当前网址同源"
  • window.popstate

    当活动的历史记录更改的时候,将会触发popstate事件,如果被激活的历史记录条目是通过对history.pushState()的调用创建的,或者受到对history.replaceState()的调用。popstate事件的state属性包含历史条目的状态对象的副本。

    histry.pushstate、history.replacestate方法调用不会触发window.popstate事件,只要作为浏览器行为才会触发、例如操作哦了tab页上面前进和后退,或者调用了history.back()、history.forword();

  • react-router-dom
  • react-router
  • history
  • react-router-dom

    react-router-dom在react-router核心基础上扩展了可操作DOM的api, 添加了用于跳转的Link组件、history模式下的BrowserRouter组件和hash模式下的HashRouter组件。 BrowserRouter和HashRouter,调用了history库中createBrowserHistory和createHashHistory方法

    react-router

    react路由核心包。 提供了路由的核心组件。如Router、Route、Switch等,但没有提供有关dom操作进行路由跳转的api;

    history

    可以理解为react-router的核心,也是整个路由原理的核心,里面集成了底层路由原理的实现。

    BrowserRouter源码分析

    BrowserRouter

    内部创建了一个全局的history对象<用于监听整个路由的变化>, 并且把history作为props传递的react-router的Router组件

    class BrowserRouter extends React.Component {
      history = createHistory(this.props);
      render() {
        return <Router history={this.history} children={this.props.children} />;
    

    Router

  • 构造函数中把history.location作为自己的state,并且监听了location的变化。
  • render中利用了React的Context提供了RouterContext,HistoryContext两个Context信息,供子元素使用。
  • class Router extends React.Component { static computeRootMatch(pathname) { return { path: "/", url: "/", params: {}, isExact: pathname === "/" }; constructor(props) { super(props); this.state = { location: props.history.location // This is a bit of a hack. We have to start listening for location // changes here in the constructor in case there are any <Redirect>s // on the initial render. If there are, they will replace/push when // they mount and since cDM fires in children before parents, we may // get a new location before the <Router> is mounted. // 这有点hack。 我们必须开始在构造函数中监听位置更改,以防初始渲染中存在任何<Redirect>。 // 如果有的话,它们将在安装时替换/推动,并且由于cDM在父级之前在子级中触发, // 因此在<Router>挂在之前,我们可能会获得一个新位置 // 因为子组件会比父组件更早渲染完成, 以及<Redirect>的存在, // 若是在<Router>的componentDidMount生命周期中对history.location进行监听, 则有可能在监听事件注册之前, // history.location已经由于<Redirect>发生了多次改变, 因此我们需要在<Router>的constructor中就注册监听事件 // 判断组件是否加载完成 this._isMounted = false; this._pendingLocation = null; // 如果不是服务端渲染,监听history变更 if (!props.staticContext) { // 每次路由变化 -> 触发顶层 Router 的回调事件 -> Router 进行 setState -> 向下传递 nextContext(context 中含有最新的 location) this.unlisten = props.history.listen(location => { // 组件未加载完毕,但是 location 发生的变化,暂存在 _pendingLocation 字段中 if (this._isMounted) { this.setState({ location }); } else { this._pendingLocation = location; componentDidMount() { this._isMounted = true; if (this._pendingLocation) { this.setState({ location: this._pendingLocation }); // 卸载监听器 componentWillUnmount() { if (this.unlisten) this.unlisten(); * @description * Router中的2个context由HistoryContext和RouterContext组件。 考虑到兼容性,并没有使用 React.createContext 方式 来创建 render() { return ( <RouterContext.Provider value={{ history: this.props.history, location: this.state.location, // 解析得到 包含path url params isExact 四个属性的属性默认指向了根地址 match: Router.computeRootMatch(this.state.location.pathname), // 只有StaticRouter会传staticContext用于服务端渲染HashRouter BrowserRouter 都是 null staticContext: this.props.staticContext <HistoryContext.Provider children={this.props.children || null} value={this.props.history} </RouterContext.Provider>

    Route

    Route 组件根据自身的传参,对上层 RouterContext 中的部分属性(location 和 match)进行了更新,并且如果当前路径和配置的 path 路径 match,则渲染该组件,渲染的方式有 children,component,render 三种方式,我们最常用的就是 component 方式,注意每种方式的区别

  • Route的component,render,children三个属性是互斥的
  • 优先级children>component>render
  • children在无论路由匹配与否,都会渲染
  • class Route extends React.Component { render() { return ( // 获取从RouterContext共享的context上下文 <RouterContext.Consumer> {context => { invariant(context, "You should not use <Route> outside a <Router>"); const location = this.props.location || context.location; const match = this.props.computedMatch ? this.props.computedMatch // <Switch> already computed the match for us : this.props.path ? matchPath(location.pathname, this.props) : context.match; const props = { ...context, location, match }; // 提供3种渲染组件的方式 let { children, component, render } = this.props; if (Array.isArray(children) && children.length === 0) { children = null; // 渲染逻辑 // 当props匹配了路由时,先判断是否匹配,如果不匹配就将props向下传递。 return ( <RouterContext.Provider value={props}> {props.match ? children ? typeof children === "function" ? __DEV__ ? evalChildrenDev(children, props, this.props.path) : children(props) : children : component ? React.createElement(component, props) : render ? render(props) : null : typeof children === "function" ? __DEV__ ? evalChildrenDev(children, props, this.props.path) : children(props) : null} </RouterContext.Provider> </RouterContext.Consumer>

    组件渲染逻辑如下:

    Switch

    switch 用来嵌套在Route外面,当Switch中的第一个Route匹配后就不会渲染其他的Route了 案例见switch.tsx

    class Switch extends React.Component {
      render() {
        return (
          <RouterContext.Consumer>
            {context => {
              invariant(context, "You should not use <Switch> outside a <Router>");
              const location = this.props.location || context.location;
              let element, match;
              // React.Children.forEach 对子元素做遍历
              React.Children.forEach(this.props.children, child => {
                if (match == null && React.isValidElement(child)) {
                  element = child;
                  // from具体是<Redirect /> 使用
                  const path = child.props.path || child.props.from;
                  // 判断组件是否匹配
                  match = path
                    ? matchPath(location.pathname, { ...child.props, path })
                    : context.match;
              return match
                ? React.cloneElement(element, { location, computedMatch: match })
                : null;
          </RouterContext.Consumer>
    export default Switch;
    const cache = {};
    const cacheLimit = 10000;
    let cacheCount = 0;
    function compilePath(path, options) {
      // 做一个全局缓存,确保计算出来的结果能够得到复用
      const cacheKey = `${options.end}${options.strict}${options.sensitive}`;
      const pathCache = cache[cacheKey] || (cache[cacheKey] = {});
      if (pathCache[path]) return pathCache[path];
      const keys = [];
      // 将字符串路径转化成为表达式
      const regexp = pathToRegexp(path, keys, options);
      const result = { regexp, keys };
      // 做多缓存 10000 个
      if (cacheCount < cacheLimit) {
        pathCache[path] = result;
        cacheCount++;
      return result;
    function matchPath(pathname, options = {}) {
      // 规范结构体
      if (typeof options === "string" || Array.isArray(options)) {
        options = { path: options };
      const { path, exact = false, strict = false, sensitive = false } = options;
      // 转化成数组进行判断
      const paths = [].concat(path);
      return paths.reduce((matched, path) => {
        if (!path && path !== "") return null;
        if (matched) return matched;
        // exact: 如果为 true,则只有在路径完全匹配 location.pathname 时才匹配。 
        // strict: 在确定为位置是否与当前 URL 匹配时,将考虑位置 pathname 后的斜线。
        // sensitive: 如果路径区分大小写,则为 true ,则匹配
        const { regexp, keys } = compilePath(path, {
          end: exact,
          strict,
          sensitive
        const match = regexp.exec(pathname);
        if
    
    
    
    
        
     (!match) return null;
        const [url, ...values] = match;
        const isExact = pathname === url;
        if (exact && !isExact) return null;
        return {
          path, // the path used to match
          url: path === "/" && url === "" ? "/" : url, // the matched portion of the URL
          isExact, // whether or not we matched exactly
          params: keys.reduce((memo, key, index) => {
            memo[key.name] = values[index];
            return memo;
          }, {})
      }, null);
    

    通过审查元素发现,link最终后悔创建一个a标签来包裹要跳转的元素的元素,但是如果只是一个普通的带 href 的 a 标签,那么就会直接跳转到一个新的页面而不是 SPA 了。所以a标签中的默认跳转事件会被禁止调。所以这里的 href 并没有实际的作用,但仍然可以标示出要跳转到的页面的 URL 并且有更好的 html 语义

    对没有被 “preventDefault调用 && 鼠标左键点击的 && 非 _blank 跳转 的&& 没有按住其他功能键的“ 单击进行 preventDefault,然后 push 进 history 中,这也是前面讲过的 —— 路由的变化 与 页面的跳转 是不互相关联的,react-router中 Link 中通过 history 库的 push 调用了 H5 history 的 pushState,但是这仅仅会让路由变化,其他什么都没有改变。 之前在Router创建 listen,它会监听路由的变化,然后通过 context 更新 props 和 nextContext 让下层的 Route 去重新匹配,完成需要渲染部分的更新

    点击时候进行如下判断。当下面4个条件都满足调用navigate方法。否则新窗口打开

  • event.defaultPrevented: 返回一个boolean,表明当前事件是否调用了event.preventDefault()
  • event.button === 0 鼠标左键
  • target === "_self" 非_blank跳转
  • !isModifiedEvent: 点击事件发生时候,没有同时按住metaKey, altKey, ctrlKey, shiftKey
  • Refs转发

    function isModifiedEvent(event) { return !!(event.metaKey || event.altKey || event.ctrlKey || event.shiftKey); / React 15 compat const forwardRefShim = C => C; let { forwardRef } = React; if (typeof forwardRef === "undefined") { forwardRef = forwardRefShim; function isModifiedEvent(event) { return !!(event.metaKey || event.altKey || event.ctrlKey || event.shiftKey); const LinkAnchor = forwardRef( innerRef, // TODO: deprecate navigate, onClick, ...rest forwardedRef
    ) => { const { target } = rest; let props = { ...rest, onClick: event => { try { if (onClick) onClick(event); } catch (ex) { event.preventDefault(); throw ex; !event.defaultPrevented && // onClick prevented default event.button === 0 && // ignore everything but left clicks (!target || target === "_self") && // let browser handle "target=_blank" etc. !isModifiedEvent(event) // ignore clicks with modifier keys // event.preventDefault()阻止超链接默认事件, 避免点击<Link>后重新刷新页面; event.preventDefault(); navigate(); if (forwardRefShim !== forwardRef) { props.ref = forwardedRef || innerRef; } else { props.ref = innerRef; // 渲染了一个没有默认跳转行为a标签,跳转行为由navigate实现 return <a {...props} />; * The public API for rendering a history-aware <a>. const Link = forwardRef( component = LinkAnchor, replace, innerRef, // TODO: deprecate ...rest forwardedRef ) => { return ( <RouterContext.Consumer> {context => { invariant(context, "You should not use <Link> outside a <Router>"); const { history } = context; // 生成location对象 const location = normalizeToLocation( resolveToLocation(to, context.location), context.location // 拼接完整路径 basename+path const href = location ? history.createHref(location) : ""; const props = { ...rest, href, navigate() { const location = resolveToLocation(to, context.location); const method = replace ? history.replace : history.push; // 执行history.push 或者 history.replace 默认 push method(location); if (forwardRefShim !== forwardRef) { props.ref = forwardedRef || innerRef; } else { props.innerRef = innerRef; return React.createElement(component, props); </RouterContext.Consumer>

    withRouter

    withRouter是一个高阶组件, 可以让普通非包裹在Route的组件也能获取路由信息。把react-router 的 history、location、match 三个对象传入 props上

    高阶组件: 高阶组件(HOC)是React中用于复用组件逻辑的一种高级技巧、自己不是React Api的一部分, 是基础React组合特性行为的设计模式

    function withRouter(Component) { const displayName = `withRouter(${Component.displayName || Component.name})`; const C = props => { const { wrappedComponentRef, ...remainingProps } = props; return ( <RouterContext.Consumer> {context => { invariant( context, `You should not use <${displayName} /> outside a <Router>` {/* 把context注入到Component中 */} return ( <Component {...remainingProps} {...context} ref={wrappedComponentRef} </RouterContext.Consumer> C.displayName = displayName; C.WrappedComponent = Component; if (__DEV__) { C.propTypes = { wrappedComponentRef: PropTypes.oneOfType([ PropTypes.string, PropTypes.func, PropTypes.object * @description * 当给组件添加至高阶组件中后,原来的组件会被一组容器组件包含。这样意味着。容器组件不会有原来组件的任何的静态方法 * 为了解决这个问题, 在返回容器组件之前。务必复制wrappedComponent的静态方法到容器组件上 return hoistStatics(C, Component);

    有时候React组件定义的静态很有有用,务必复制静态方法

    Redirect

    该组件在componentDidMount生命周期内,通过history Api跳转到path指定位置, 默认情况下,新位置将覆盖历史堆栈中的当前位置。

    function Redirect({ computedMatch, to, push = false }) {
      return (
        <RouterContext.Consumer>
          {context => {
            invariant(context, "You should not use <Redirect> outside a <Router>");
            const { history, staticContext } = context;
            const method = push ? history.push : history.replace;
            // to 重定向地址,可以是一个string, 也可以是对象
            // computedMatch 从switch上面拿到match
            const location = createLocation(
              computedMatch
                ? typeof to === "string"
                  ? generatePath(to, computedMatch.params)
                      ...to,
                      pathname: generatePath(to.pathname, computedMatch.params)
            // 服务端渲染直接执行方法一次
            if (staticContext) {
              method(location);
              return null;
            // lifeCycle不会渲染任何页面, 只有一些生命周期函数componentDidMount、componentDidUpdate、componentWillUnmount
            return (
              <Lifecycle
                onMount={() => {
                  method(location);
                // componentDidUpdate 时候判断当前 location 和上一个 location 是否发生变化
                // 一般来说在componentDidMount就调走了。不会走到ComponentDidUpdate
                onUpdate={(self, prevProps) => {
                  const prevLocation = createLocation(prevProps.to);
                    !locationsAreEqual(prevLocation, {
                      ...location,
                      key: prevLocation.key
                    method(location);
                to={to}
        </RouterContext.Consumer>
    

    Prompt

    用于路由切换提示,在某些场景下比较有用,比较用户咋某个页面修改数据。离开页面的时候,提示用户是保存。 详细在history中block明确解释

  • message:用于显示提示的文本信息。
  • when:默认是 true,设置成 false 时,失效
  • Prompt 的本质是在 when 为 true 的时候,调用 context.history.block 方法,为全局注册路由监听。见prompt.tsx

    function Prompt({ message, when = true }) {
      return (
        <RouterContext.Consumer>
          {context => {
            invariant(context, "You should not use <Prompt> outside a <Router>");
            if (!when || context.staticContext) return null;
            // 调用history.block注册全局路由监听器
            // message可以是字符串也可以是一个函数, 如果是字符串默认调用window.confirm方法
            // 如果是一个函数,需要返回一个boolean判断是否需要拦截
            const method = context.history.block;
            return (
              <Lifecycle
                onMount={self => {
                  // 调用了 history.block 方法
                  self.release = method(message);
                onUpdate={(self, prevProps) => {
                  if (prevProps.message !== message) {
                    self.release();
                    self.release = method(message);
                onUnmount={self => {
                  self.release();
                message={message}
        </RouterContext.Consumer>
    

    hooks

    hooks是react16.8引入的特性,允许我们在不写class的情况下,操作state和react其他特新。为了在hooks即函数式组件能够操作路由。react-router提供了hooks方法, 底层都是使用React.useContext api <可以获取指定context的值>

  • useHistory: 返回一个history对象
  • useLocation 返回context下的location对象
  • useParams 返回当前匹配路径的params
  • useRouteMatch 可以有一个参数 path,如果什么都不传,会返回当前 context 上的 match 的值。 如果传了 path,会比较这个 path 和当前 location 是否 match
  • const useContext = React.useContext;
    export function useHistory() {
      if (__DEV__) {
        invariant(
          typeof useContext === "function",
          "You must use React >= 16.8 in order to use useHistory()"
      return useContext(HistoryContext);
    export function useLocation() {
      if (__DEV__) {
        invariant(
          typeof useContext === "function",
          "You must use React >= 16.8 in order to use useLocation()"
      return useContext(Context).location;
    export function useParams() {
      if (__DEV__) {
        invariant(
          typeof useContext === "function",
          "You must use React >= 16.8 in order to use useParams()"
      const match = useContext(Context).match;
      return match ? match.params : {};
    export function useRouteMatch(path) {
      if (__DEV__) {
        invariant(
          typeof useContext === "function",
          "You must use React >= 16.8 in order to use useRouteMatch()"
      const location = useLocation();
      const match = useContext(Context).match;
      return path ? matchPath(location.pathname, path) : match;
    
  • Router初始化,创建监听函数, 底层逻辑全部由history库管理
  • 当点击Link标签的时候,实际上点击是页面渲染出来的a标签。通过preventDefault来组织a标签的页面跳转
  • Link中拿到context传递的history, 进行路由跳转
  • 路由发生变化,触发了监听函数,Router会重新setState, 每次路由变化 -> 触发顶层 Router 的监听事件 -> Router 触发 setState -> 向下传递新的 nextContext
  • 下层Route组件拿到最新nextContext后判断当前path和location是否匹配。内置component,render,children三个属性的渲染机制,并且通过switch组件匹配唯一的路由
  • history库源码分析

    原理就是封装了原生的html5 的history api, 如pushState, replaceState。当这些事件被触发的时候,执行响应回调函数,用于操控和观察地址栏的变更

    history库创建了一个虚拟的history对象, 操纵浏览器地址变更,或者操作hash变更、管理内存中的虚拟历史堆栈

    utils

    pathUtils

      // 对传递的path首部添加/
      function addLeadingSlash(path) {
        return path.charAt(0) === '/' ? path : '/' + path;
      // 对传递path去掉首部的/
      function stripLeadingSlash(path) {
        return path.charAt(0) === '/' ? path.substr(1) : path;
      // 判断path中是否包含basename
      function hasBasename(path, prefix) {
        return path.toLowerCase().indexOf(prefix.toLowerCase()) === 0 && '/?#'.indexOf(path.charAt(prefix.length)) !== -1
    
    
    
    
        
    ;
      // 如果传递了pathname, 把path中首部basename去掉
      function stripBasename(path, prefix) {
        return hasBasename(path, prefix) ? path.substr(prefix.length) : path;
      // 去掉尾部的/
      function stripTrailingSlash(path) {
        return path.charAt(path.length - 1) === '/' ? path.slice(0, -1) : path;
      // 在createLocation调用
      // 把字符串路径path解析成 {pathname, search, hash}的对象返回出去
      function parsePath(path) {
        var pathname = path || '/';
        var search = '';
        var hash = '';
        var hashIndex = pathname.indexOf('#');
        if (hashIndex !== -1) {
          hash = pathname.substr(hashIndex);
          pathname = pathname.substr(0, hashIndex);
        var searchIndex = pathname.indexOf('?');
        if (searchIndex !== -1) {
          search = pathname.substr(searchIndex);
          pathname = pathname.substr(0, searchIndex);
        return {
          pathname: pathname,
          search: search === '?' ? '' : search,
          hash: hash === '#' ? '' : hash
      // 把location对象<{pathname, search, hash}> 生成最终的地址栏路径
      function createPath(location) {
        var pathname = location.pathname,
            search = location.search,
            hash = location.hash;
        var path = pathname || '/';
        if (search && search !== '?') path += search.charAt(0) === '?' ? search : "?" + search;
        if (hash && hash !== '#') path += hash.charAt(0) === '#' ? hash : "#" + hash;
        return path;
    

    domUtils

    // 是否可以操作DOM节点,即判断window.document对象是否存在 var canUseDOM = !!(typeof window !== 'undefined' && window.document && window.document.createElement); // 路由跳转拦截回调函数,默认使用window.confirm function getConfirmation(message, callback) { callback(window.confirm(message)); // eslint-disable-line no-alert * Returns true if the HTML5 history API is supported. Taken from Modernizr. * https://github.com/Modernizr/Modernizr/blob/master/LICENSE * https://github.com/Modernizr/Modernizr/blob/master/feature-detects/history.js * changed to avoid false negatives for Windows Phones: https://github.com/reactjs/react-router/issues/586 // 不支持 安卓是2. 和 4.0版本 并且ua信息包含 ’Mobile Safari‘ && Chrome && Windows Phone // 判断主流浏览器平台是否支持html5 history api function supportsHistory() { var ua = window.navigator.userAgent; if ((ua.indexOf('Android 2.') !== -1 || ua.indexOf('Android 4.0') !== -1) && ua.indexOf('Mobile Safari') !== -1 && ua.indexOf('Chrome') === -1 && ua.indexOf('Windows Phone') === -1) return false; return window.history && 'pushState' in window.history; * Returns true if browser fires popstate on hash change. * IE10 and IE11 do not. // 判断主流浏览器平台在hashchange的时候是否会触发popstate 事件, IE10,IE10并不会 function supportsPopStateOnHashChange() { return window.navigator.userAgent.indexOf('Trident') === -1; * Returns false if using go(n) with hash history causes a full page reload. // 当使用go变更hash的时候,会不会造成页面刷新 function supportsGoWithoutReloadUsingHash() { return window.navigator.userAgent.indexOf('Firefox') === -1; * Returns true if a given popstate event is an extraneous WebKit event. * Accounts for the fact that Chrome on iOS fires real popstate events * containing undefined state when pressing the back button. // 判断popstate是否是有效的 // 如果给定的popstate事件是无关的webkit事件, 则会返回true, // 在IOS上chrome会触发state为undefined 真实的popstate事件 function isExtraneousPopstateEvent(event) { return event.state === undefined && navigator.userAgent.indexOf('CriOS') === -1; var PopStateEvent = 'popstate'; var HashChangeEvent = 'hashchange'; // 返回history的state function getHistoryState() { try { // state 必须有pustate/replaceState产生,不然都是null return window.history.state || {}; } catch (e) { // IE11 下有时候会抛出异常, 返回返回的state的是一个对象 return {};

    createLocation

    生成 {pathname, search, hash, state, key} 对象

      function createLocation(path, state, key, currentLocation) {
        var location;
        if (typeof path === 'string') {
          // 分解{pathname, search, hash}对象
          location = parsePath(path);
          // 添加state属性到location上
          location.state = state;
        } else {
          location = _extends({}, path);
          // 补足location操作
          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 {
          // 尝试对pathname decodeURI 解码
          location.pathname = decodeURI(location.pathname);
        } catch (e) {
          if (e instanceof URIError) {
            // decodeURI() 函数能解码由encodeURI 创建或其它流程得到的统一资源标识符(URI)。
            // decodeURI在解析非合法的URI编码是抛出URIError类型错误
            throw new URIError('Pathname "' + location.pathname + '" could not be decoded. ' + 'This is likely caused by an invalid percent-encoding.');
          } else {
            throw e;
        if (key) location.key = key;
        if (currentLocation) {
          // Resolve incomplete/relative pathname relative to current location.
          if (!location.pathname) {
            location.pathname = currentLocation.pathname;
          } else if (location.pathname.charAt(0) !== '/') {
            location.pathname = resolvePathname(location.pathname, currentLocation.pathname);
        } else {
          // When there is no prior location and pathname is empty, set it to /
          // 如果pathname为空并且,没有currentLocation 定向到根节点
          if (!location.pathname) {
            location.pathname = '/';
        return location;
    

    createTransitionManager

    创建任务管理器=> 控制路由跳转以及添加路由监听函数

      function createTransitionManager() {
        var prompt = null;
        // 这里使用了闭包,设置了prompt,返回了清空函数, prompt可以是一个string或者函数
        function setPrompt(nextPrompt) {
          process.env.NODE_ENV !== "production" ? warning(prompt == null, 'A history supports only one prompt at a time') : void 0;
          prompt = nextPrompt;
          return function () {
            if (prompt === nextPrompt) prompt = null;
        function confirmTransitionTo(location, action, getUserConfirmation, callback) {
          // TODO: If another transition starts while we're still confirming
          // the previous one, we may end up in a weird state. Figure out the
          // best way to handle this.
          // 是否设置路由跳转拦截器
          if (prompt != null) {
            var result = typeof prompt === 'function' ? prompt(location, action) : prompt;
            if (typeof result === 'string') {
              if (typeof getUserConfirmation === 'function') {
                getUserConfirmation(result, callback);
              } else {
                process.env.NODE_ENV !== "production" ? warning(false, 'A history needs a getUserConfirmation function in order to use a prompt message') : void 0;
                callback(true);
            } else {
              // Return false from a transition hook to cancel the transition.
    
    
    
    
        
    
              callback(result !== false);
          } else {
            // 不存在pprompt,直接执行回调函数, 通知notifyListeners
            callback(true);
        // 发布订阅模式,将回调函数加入到listners
        // 存储监听函数
        var listeners = [];
        // 返回取消监听函数
        function appendListener(fn) {
          var isActive = true;
          function listener() {
            if (isActive) fn.apply(void 0, arguments);
          listeners.push(listener);
          return function () {
            isActive = false;
            listeners = listeners.filter(function (item) {
              return item !== listener;
        // 通知被订阅事件开始执行
        function notifyListeners() {
          for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
            args[_key] = arguments[_key];
          // 依次遍历执行监听器数组每个注册的事件
          listeners.forEach(function (listener) {
            return listener.apply(void 0, args);
        return {
          setPrompt: setPrompt,
          confirmTransitionTo: confirmTransitionTo,
          appendListener: appendListener,
          notifyListeners: notifyListeners
    

    createBrowserHistory

    if (props === void 0) { props = {}; !canUseDOM ? process.env.NODE_ENV !== "production" ? invariant(false, 'Browser history needs a DOM') : invariant(false) : void 0; // 拿到全局的history对象 var globalHistory = window.history; // 不支持 安卓是2. 和 4.0版本 并且ua信息包含 ’Mobile Safari‘ && Chrome && Windows Phone var canUseHistory = supportsHistory(); // 当hash改变时,如果不能触发popstate事件,则添加hashchange事件当hash改变时,如果不能触发popstate事件,则添加hashchange事件 var needsHashChangeListener = !supportsPopStateOnHashChange(); var _props = props, _props$forceRefresh = _props.forceRefresh, // 默认切换路由不刷新 forceRefresh = _props$forceRefresh === void 0 ? false : _props$forceRefresh, // 初始化是否注入getUserConfirmation函数,默认是window.confirm _props$getUserConfirm = _props.getUserConfirmation, getUserConfirmation = _props$getUserConfirm === void 0 ? getConfirmation : _props$getUserConfirm, _props$keyLength = _props.keyLength, // 默认6位长度随机key keyLength = _props$keyLength === void 0 ? 6 : _props$keyLength; // 添加basename 同时首部添加/ 去掉尾部/ eg: /ahs/xxx var basename = props.basename ? stripTrailingSlash(addLeadingSlash(props.basename)) : ''; function getDOMLocation(historyState) { // 获取history对象的key和state var _ref = historyState || {}, key = _ref.key, state = _ref.state; var _window$location = window.location, pathname = _window$location.pathname, search = _window$location.search, hash = _window$location.hash; // 拼一下完整的路径 var path = pathname + search + hash; process.env.NODE_ENV !== "production" ? warning(!basename || hasBasename(path, basename), 'You are attempting to use a basename on a page whose URL path does not begin ' + 'with the basename. Expected path "' + path + '" to begin with "' + basename + '".') : void 0; // 去掉path中的basename if (basename) path = stripBasename(path, basename); // 生成一个自定义location对象 return createLocation(path, state, key); // 创建36进制的随机数key 从第2位开始截取 function createKey() { return Math.random().toString(36).substr(2, keyLength); var transitionManager = createTransitionManager(); // 路由发生改变的时候,更新history的部分属性,如action, location等,在路由完成跳转后 // 通知transitionManager触发所有监听函数 function setState(nextState) { _extends(history, nextState); // 更新history的length, 实实保持和window.history.length 同步 history.length = globalHistory.length; // 推送至订阅者,执行响应回调函数 transitionManager.notifyListeners(history.location, history.action); // 监听popState事件进行处理<过滤掉IOS上无效的popstate事件,即: state为undefined> function handlePopState(event) { if (isExtraneousPopstateEvent(event)) return; // 拿到当前地址的event.state 传递给getDOMLocation。得到最新location对象 handlePop(getDOMLocation(event.state)); function handleHashChange() { // 监听到hashchange时进行的处理,由于hashchange不会更改state // 此处不需要更新location的state handlePop(getDOMLocation(getHistoryState())); // 是否强制路由加载 var forceNextPop = false; // handlePop是对使用go方法来回退或者前进时,对页面进行的更新,正常情况下来说没有问题 // 但是如果页面使用Prompt,即路由拦截器。当点击回退或者前进就会触发histrory的api,改变了地址栏的路径 // 然后弹出需要用户进行确认的提示框,如果用户点击确定,那么没问题因为地址栏改变的地址就是将要跳转到地址 // 但是如果用户选择了取消,那么地址栏的路径已经变成了新的地址,但是页面实际还停留再之前,这就产生了bug // 这也就是 revertPop 这个hack的由来。因为页面的跳转可以由程序控制,但是如果操作的本身是浏览器的前进后退 function handlePop(location) { if (forceNextPop) { forceNextPop = false; setState(); } else { var action = 'POP'; transitionManager.confirmTransitionTo(location, action, getUserConfirmation, function (ok) { if (ok) { setState({ action: action, location: location } else { // 回滚 revertPop(location); // https://github.com/ReactTraining/history/issues/690 // 这里是react-router的作者最头疼的一个地方,因为虽然用hack实现了表面上的路由拦截 // ,但也会引起一些特殊情况下的bug。这里先说一下如何做到的假装拦截,因为本身html5 history // api的特性,pushState 这些操作不会引起页面的reload,所有做到拦截只需要不手懂调用setState页面不进行render即可 // 当用户选择了取消后,再将地址栏中的路径变为当前页面的显示路径即可,这也是revertPop实现的方式 // 这里贴出一下对这个bug的讨论:https://github.com/ReactTraining/history/issues/690 // fromLocation 当前 function revertPop(fromLocation) { // fromLocation 当前地址栏真正的地址 var toLocation = history.location; // TODO: We could probably make this more reliable by // keeping a list of keys we've seen in sessionStorage. // Instead, we just default to 0 for keys we don't know. // allKeys 缓存历史堆栈数据 取出来 formLocaton 和 当天 histoty.location 维护的key在 allKeys索引中的位置 var toIndex = allKeys.indexOf(toLocation.key); if (toIndex === -1) toIndex = 0; var fromIndex = allKeys.indexOf(fromLocation.key); if (fromIndex === -1) fromIndex = 0; // 两者进行相减的值就是go操作需要回退或者前进的次数 var delta = toIndex - fromIndex; // 如果delta不为0 则进行过地址栏的变更。 浏览器历史记录重定向到当前页面的路径 if (delta) { // 将forceNextPop设置为true // 更改地址栏的路径,又会触发handlePop 方法,此时由于forceNextPop已经为true则会执行后面的 // setState方法,对当前页面进行rerender,注意setState是没有传递参数的,这样history当中的 // location对象依然是之前页面存在的那个loaction,不会改变history的location数据 forceNextPop = true; go(delta); // 初始化一个location对象 var initialLocation = getDOMLocation(getHistoryState()); var allKeys = [initialLocation.key]; // Public interface function createHref(location) { return basename + createPath(location); function push(path, state) { // path 可以是字符串也可以是对象,当path传递是对象,包含state并且第二个参数state也存在,会扔出警告,第二个state将会被忽略掉 process.env.NODE_ENV !== "production" ? warning(!(typeof path === 'object' && path.state !== undefined && state !== undefined), 'You should avoid providing a 2nd state argument to push when the 1st ' + 'argument is a location-like object that already has state; it is ignored') : void 0; var action = 'PUSH'; // 返回一个对象包含 pathname,search,hash,state,key var location = createLocation(path, state, createKey(), history.location); // 路由的切换,最后一个参数为回调函数,只有返回true的时候才会进行路由的切换 transitionManager.confirmTransitionTo(location, action, getUserConfirmation, function (ok) { if (!ok) return; var href = createHref(location); // 拼接basename后的完整路径 var key = location.key, // 随机生成key值 state = location.state; // 获取新的location中的key和state // 当可以使用原生的html5 history api的,调用原生的history.pushstate方法更改浏览器地址栏路径 // 此时只是改变地址栏路径 页面并不会发生变化 需要手动setState从而rerender if (canUseHistory) { globalHistory.pushState({ key: key, state: state }, null, href); // 是否开启强制刷新 if (forceRefresh) { window.location.href = href; } else { // 获取上次访问key的下标 var prevIndex = allKeys.indexOf(history.location.key ); // 当下标存在时,返回截取到当前下标的数组key列表的一个新引用,不存在则返回一个新的空数组 // 这样做的原因是什么?为什么不每次访问直接向allKeys列表中直接push要访问的key // 比如这样的一种场景, 1-2-3-4 的页面访问顺序,这时候使用go(-2) 回退到2的页面,假如在2 // 的页面我们选择了push进行跳转到4页面,如果只是简单的对allKeys进行push操作那么顺序就变成了 // 1-2-3-4-4,这时候就会产生一悖论,从4页面跳转4页面,这种逻辑是不通的,所以每当push或者replace // 发生的时候,一定是用当前地址栏中path的key去截取allKeys中对应的访问记录,来保证不会push连续相同的页面 var nextKeys = allKeys.slice(0, prevIndex + 1); nextKeys.push(location.key); allKeys = nextKeys; // 通知事件调度中心,执行相对应监听函数,重新render页面 setState({ action: action, location: location // 新的location } else { process.env.NODE_ENV !== "production" ? warning(state === undefined, 'Browser history cannot push state in browsers that do not support HTML5 history') : void 0; window.location.href = href; function replace(path, state) { process.env.NODE_ENV !== "production" ? warning(!(typeof path === 'object' && path.state !== undefined && state !== undefined), 'You should avoid providing a 2nd state argument to replace when the 1st ' + 'argument is a location-like object that already has state; it is ignored') : void 0; var action = 'REPLACE'; var location = createLocation(path, state, createKey(), history.location); transitionManager.confirmTransitionTo(location, action, getUserConfirmation, function (ok) { if (!ok) return; var href = createHref(location); var key = location.key, state = location.state; if (canUseHistory) { globalHistory.replaceState({ key: key, state: state }, null, href); if (forceRefresh) { window.location.replace(href); } else { var prevIndex = allKeys.indexOf(history.location.key); if (prevIndex !== -1) allKeys[prevIndex] = location.key; setState({ action: action, location: location } else { process.env.NODE_ENV !== "production" ? warning(state === undefined, 'Browser history cannot replace state in browsers that do not support HTML5 history') : void 0; window.location.replace(href); function go(n) { globalHistory.go(n); function goBack() { go(-1); function goForward() { go(1); var listenerCount = 0; // 防止重复注册,只有 listenerCount === 1 && delta === 1 进行监听事件 // 同时在window上设置popstate、pushState 监听函数 // delta=-1 移除window对象上popState pushState 等事件 function checkDOMListeners(delta) { listenerCount += delta; if (listenerCount === 1 && delta === 1) { window.addEventListener(PopStateEvent, handlePopState); if (needsHashChangeListener) window.addEventListener(HashChangeEvent, handleHashChange); } else if (listenerCount === 0) { window.removeEventListener(PopStateEvent, handlePopState); if (needsHashChangeListener) window.removeEventListener(HashChangeEvent, handleHashChange); var isBlocked = false; // 设置路由跳转拦截监听器,这里的block专门为prompt组件服务,开发者可以模拟对路由的拦截 // [Remove history.block #690](https://github.com/remix-run/history/issues/690) function block(prompt) { if (prompt === void 0) { prompt = false; var unblock = transitionManager.setPrompt(prompt); // 监听事件只会当拦截器开启时被注册,同时设置isBlock为true,防止多次注册 if (!isBlocked) { checkDOMListeners(1); isBlocked = true; // 返回关闭路由拦截的方法 return function () { if (isBlocked) { isBlocked = false; checkDOMListeners(-1); return unblock(); // 添加自定义监听函数,并返回取消监听函数 function listen(listener) { var unlisten = transitionManager.appendListener(listener); // 添加订阅者 checkDOMListeners(1); return function () { checkDOMListeners(-1); unlisten(); var history = { length: globalHistory.length, // 存储浏览器历史堆栈中数量 action: 'POP', // 执行的方法名 location: initialLocation, // 保存的location对象 {pathname, search, hash, state, key }构成 createHref: createHref, // 构成完整的浏览器路径+basename push: push, // 自定义push事件, 实现路由跳转 replace: replace, // 自定义replace事件, 实现路由跳转, 替换历史记录堆栈数据 go: go, // 调用history.go方法 goBack: goBack, // 调用history.go方法 goForward: goForward, // 调用history.go方法 block: block, // 设置路由跳转拦截监听函数 listen: listen // 添加自定义路由监听函数 return history;
  • 首先BrowserRouter通过history库创建了history对象,并且把此对象通过props形势传递Router组件
  • Router组件使用hisroty中listen,注册了资深的setState方法,当路由发生发生改变的时候<出发popstate,手动push>组件就会执行setState方法,完成整个组件数的render
  • history是一个对象 包含了各种操作页面的方法。 同时会用Router的props里面forceRefresh、basename、getUserComfirmation、keyLength 来生成一个初始化的location对象
  • 拿到从初始化的location对象,history开始封装push,replace,go,goback等方法。对于任何地址栏上的更新,都会执行confirmTransitionTo验证,这个方法是为了支持prompt拦截器功能, 正常在拦截器关闭的情况下,每次调用push或者replace都会随机生成一个key,代表这个路径的唯一hash值,并将用户传递的state和key作为state,注意这部分state会被保存到 浏览器 中是一个长效的缓存,将拼接好的path作为传递给history的第三个参数
  • 地址栏地址得到更新后,页面在不使用foreceRefrsh的情况下是不会自动更新的, 需要执行setState, 同时执行调度中心里面监听函数
  • history有block方法,这个方法初衷是实现对路由跳转的拦截,我们知道浏览器的回退和前进操作按钮是无法进行拦截的, 只能做hack。为了history抽离了一个路由控制器createTransitionManager,里面维护了一个prompt开关, 每当prompt存在的时候,默认会被window.confirm拦截,如果确认拦截,则页面仍然会停留在当前页面中,但是地址栏已经更新了地址,这就产生了矛盾。需要把地址栏路径重置为之前的路径。为了实现这功能。都会在allkeys找到当前key对应的下表,以及之前页面key对应的下标。以两者下标差做一个回滚 正是因为有了Prompt才会促使history添加key
  • 手动实现mini-router

    utils

    export interface Path { hash: string search: string pathname: string export function parsePath(path: string) { const partialPath = {} as Path const pathname = path if (path) { const hashIndex = path.indexOf('#') if (hashIndex >= 0) { partialPath.hash = path.substr(hashIndex) pathname = path.substr(0, hashIndex) const searchIndex = path.indexOf('?') if (searchIndex >= 0) { partialPath.search = path.substr(searchIndex) pathname = path.substr(0, searchIndex) if (path) { partialPath.pathname = pathname return partialPath

    history

    import { parsePath } from './utils' export interface History { push(): void export type State = object | null export type Listener = (location: Location) => void export interface Location { state: State hash: string search: string pathname: string const getLocation = (): Location => { const { pathname, search, hash } = window.location return { pathname, search, hash, state: null, let location = getLocation() const getNextLocation = (to: string, state: State = null) => ({ ...parsePath(to), state, const listeners: Listener[] = [] const push = (to: string, state?: State) => { location = getNextLocation(to, state) window.history.pushState(state, '', to) listeners.forEach((fn) => fn(location)) // 设置监听函数 const listen = (fn: Listener) => { listeners.push(fn) return function unlisten() { listeners = listeners.filter((listener) => listener !== fn) // 处理浏览器的前进后退 window.addEventListener('popstate', () => { location = getLocation() listeners.forEach((fn) => fn(location)) export const history = { get location() { return location push, listen,

    Router

    import { history, Location } from './history' import React, { useState, useEffect } from 'react' interface RouterContextProps { location: Location history: typeof history export const RouterContext = React.createContext<RouterContextProps | null>(null) export const Router: React.FC = ({ children }) => { const [location, setLocation] = useState(history.location) useEffect(() => { const unlisten = history.listen((newLocation) => { setLocation(newLocation) return unlisten }, []) return <RouterContext.Provider value={{ history, location }}>{children}</RouterContext.Provider>

    Route

    import { ReactNode } from 'react'
    import { useLocation } from './hooks'
    interface RouteProps {
      path: string
      children: ReactNode
    export const Route = ({ path, children }: RouteProps) => {
      const { pathname } = useLocation()
      const matched = path === pathname
      if (matched) {
        return children
      return null
    

    hooks

    import React from 'react' import { RouterContext } from './Router' export const useHistory = () => React.useContext(RouterContext)!.history export const useLocation = () => React.useContext(RouterContext)!.location import React from 'react' import { Card, Button, Divider } from 'antd' import { Router, Route, useHistory } from './customizeRouter/index' const List = () => <h1>列表</h1> const Detail = () => <h1>详情</h1> const About = () => <h1>关于我</h1> const RenderRoute = () => { const history = useHistory() const go = (path: string) => { const state = { name: Math.random().toString(36).substr(2, 8) } history.push(path, state) return ( <Button type="link" size="small" onClick={() => go('/list')}> </Button> <Button type="link" size="small" onClick={() => go('/detail')}> </Button> <Button type="link" size="small" onClick={() => go('/about')}> </Button> </div> export default () => ( <Card title="自定义mini-router"> <Router> <RenderRoute /> <Divider /> <Route path="/list"> <List /> </Route> <Route path="/detail"> <Detail /> </Route> <Route path="/about"> <About /> </Route> </Router> </Card>
  • 在 React Router 中使用 JWT
  • 面试官为啥总是喜欢问前端路由实现方式?
  • 2022 年的 React 生态
  •