前端Router原理&简单实现

本文源自于面试题:如果让你设计前端Router,你会怎么去做?

接下去将从常见的前端路由模式,以及 vue 和 react 两个框架的router开始,实现简单的router。

路由模式

Hash模式

Hash模式是 html5 以前常用的路由模式,具有以下特点:

  • 在实际的URL路径前,使用哈希字符(#)进行分割。

  • hash路由向服务器发送请求,请求的是#以前的URL,因此不需要在服务器层面做任何处理。

  • 使用的是监听hashChange的方法来监听hash的变化。

  • 浏览器兼容性好。

History模式

相比较Hash模式,History具有以下特点:

  • 没有烦人的'#'号,SEO友好。

  • 在SPA应用中,需要服务器端的配置(Nginx try_files),否则会发生404错误。

  • 使用的是html5 新增的popstate事件去监听pathname的变化。

  • 向下兼容到IE10。

react在此基础上创建了Browser Router

Memory模式

基于内存的路由,Vue应用场景主要是用于处理服务器端渲染,为SSR提供路由历史。可以当做测试用例去看待

其他

还有native router、static router。

Vue Router

vue-router 原理

插件的注入

import { createApp } from 'vue';
import App from './App.vue';
import router from './router';

createApp(App).use(router).mount('#app');

可以看到 vue-router 是通过app.use()来使用的,这是 vue 用来安装一个插件的方法,那么什么是插件?

插件是一个拥有install()方法的对象,也可以是一个安装函数本身。

所以 vue-router 的重点就是 install 方法做了什么。

首先使用app.component方法注册两个组件,RouterLinkRouterView。它们的使用在这里就不多赘述。

app.component('RouterLink', RouterLink);
app.component('RouterView', RouterView);

然后在全局注册了$router实例,并且this指向我们当前的router,所以我们可以通过this.$router访问到这个实例。

const router = this;
app.config.globalProperties.$router = router;

接着对全局注册的$route进行了监听,通过get获取到我们当前的route

Object.defineProperty(app.config.globalProperties, '$route', {
    enumerable: true,
    get: () => vue.unref(currentRoute),
});

再往下是针对 vue3 多实例的一个处理,判断浏览器环境,并且初始化导航的情况下去执行后面的内容,目的是避免多实例对router创建造成的影响。

if (isBrowser &&
    !started &&
    currentRoute.value === START_LOCATION_NORMALIZED) {
    started = true;
    push(routerHistory.location).catch(err => {
        warn('Unexpected error when starting the router:', err);
    });
}

接下去是一些添加响应式的操作,其实就是对于路由上下文进行的监听。

const reactiveRoute = {};
for (const key in START_LOCATION_NORMALIZED) {
    Object.defineProperty(reactiveRoute, key, {
        get: () => currentRoute.value[key],
        enumerable: true,
    });
}

其中,START_LOCATION_NORMALIZED定义如下:

const START_LOCATION_NORMALIZED = {
    path: '/',
    name: undefined,
    params: {},
    query: {},
    hash: '',
    fullPath: '/',
    matched: [],
    meta: {},
    redirectedFrom: undefined,
};

再往后,使用了app.provide方法,将上面监听、创建的变量挂载到app实例上,以便我们后面去访问。

这里其实也是针对 vue3 的兼容处理,因为 composition API 中无法获取到this

app.provide(routerKey, router);
app.provide(routeLocationKey, vue.shallowReactive(reactiveRoute));
app.provide(routerViewLocationKey, currentRoute);

最后就是app.unmount触发时的处理,这里也做了多实例的兼容。

这里用了installedApps,它是一个Set数据结构,当对应的app实例触发卸载生命周期时删除集合中的app

当没有app时,将我们的状态变为初始值。

const unmountApp = app.unmount;
installedApps.add(app);
app.unmount = function () {
    installedApps.delete(app);
    if (installedApps.size < 1) {
        pendingLocation = START_LOCATION_NORMALIZED;
        removeHistoryListener && removeHistoryListener();
        removeHistoryListener = null;
        currentRoute.value = START_LOCATION_NORMALIZED;
        started = false;
        ready = false;
    }
    unmountApp();
};

路由的实现

主要就是createWebHashHistorycreateMemoryHistorycreateWebHistory三个API。

在路由模式里已经说了实现思路,即如何监听URL的变化,进行对应的更新。

  • Hash

主要就是在路径前拼接'#'

function createWebHashHistory(base) {
    base = location.host ? base || location.pathname + location.search : '';
    if (!base.includes('#'))
        base += '#';
    if (!base.endsWith('#/') && !base.endsWith('#')) {
        warn(`A hash base must end with a "#":\n"${base}" should be "${base.replace(/#.*$/, '#')}".`);
    }
    return createWebHistory(base);
}
  • History

function createWebHistory(base) {
    base = normalizeBase(base);
    const historyNavigation = useHistoryStateNavigation(base);
    const historyListeners = useHistoryListeners(base, historyNavigation.state, historyNavigation.location, historyNavigation.replace);
    function go(delta, triggerListeners = true) {
        if (!triggerListeners)
            historyListeners.pauseListeners();
        history.go(delta);
    }
    const routerHistory = assign({
        location: '',
        base,
        go,
        createHref: createHref.bind(null, base),
    }, historyNavigation, historyListeners);
    Object.defineProperty(routerHistory, 'location', {
        enumerable: true,
        get: () => historyNavigation.location.value,
    });
    Object.defineProperty(routerHistory, 'state', {
        enumerable: true,
        get: () => historyNavigation.state.value,
    });
    return routerHistory;
}

手写实现

根据使用的方法,我们需要创建一个类,其中包含moderoutes属性。

class VueRouter {
  constructor(options) {
    this.mode = options.mode || 'hash'; // 默认hash模式
    this.routes = options.routes || [];
    this.init()
  }
}

init方法中,我们要进行路由初始化,对于Hash模式,我们监听到load事件后,就要获取/之后的路径

init() {
  if (this.mode === 'hash') {
    // 针对没有hash时,设置为/#/
    location?.hash ? '' : (location.hash = '/')
    window.addEventListener('load', () => {
      location.hash.slice(1)
    });
  }
}

接着我们要创建一个类来维护我们的路由上下文,并在Router类中创建history实例,将获取到的路径加入到上下文中。

class HistoryRoute {
  constructor() {
    this.current = null;
  }
}

所以针对hash路由的初始化如下,包括对于hashChange的监听:

if (this.mode === 'hash') {
  // 针对没有hash时,设置为/#/
  location?.hash ? '' : (location.hash = '/')
  window.addEventListener('load', () => {
    this.history.current = location.hash.slice(1)
  });
  window.addEventListener('hashChange', () => {
    this.history.current = location.hash.slice(1)
  });
}

照上面的思路,针对history路由的初始化:

else {
  location.pathname ? '' : (location.pathname = '/')
  window.addEventListener('load', () => {
    this.history.current = location.pathname
  });
  window.addEventListener('popState', () => {
    this.history.current = location.pathname
  });
}

我们还要将pathcomponent进行关联,以键值对的方式存储,在constructor中,创建Map

this.routesMap = this.createMap(this.routes);
createMap(routes) {
  return routes.reduce((pre, cur) => {
    pre[cur.path] = cur.component;
    return pre;
  }, {})
  // 返回的形式如下
  // {
  //   ['/home']: Home,
  //   ['/about']: About
  // }
}

接下去我们要提供install方法,并且按照源码里的思路去挂载我们的实例,这里用mixin来实现。

我们在beforeCreate声明周期判断是否挂载到根实例上。

install(v) {
  Vue = v;
  Vue.mixins({
    beforeCreate() {
      if (this.$options && this.$options.router) {
        // 已经挂载到根实例
        this._root = this;
        this._router = this.$options.router
      } else {
        // 子组件
        this._root = this.$parent && this.parent._root
      }
    }
  })
}

然后对$router$route进行创建和监听

Object.defineProperty(this, '$router', {
  get() {
    return this._root._router;
  }
})
Object.defineProperty(this, '$route', {
  get() {
    return this._root._router.history.current;
  }
})

最后,要创建两个RouterViewRouterLink两个组件,,通过render(h)的方式创建,其中,RouterLink最终要渲染成a标签,将to属性转变为href属性。

Vue.component('router-link', {
  props: {
    to: String,
  },
  render(h) {
    let mode = this._self._root._router.mode;
    let to = mode === 'hash' ? '#' + this.to : this.to;
    return h('a', { attrs: { href: to } }, this.$slots.default);
  },
});
Vue.component('router-view', {
  render(h) {
    let current = this._self._root._router.history.current;
    let routeMap = this._self._root._router.routesMap;
    return h(routeMap[current]);
  },
});

完整代码如下:

let Vue = null; // 通过install注入vue实例

class HistoryRoute {
  constructor() {
    this.current = null; // 上下文状态维护
  }
}

class VueRouter {
  constructor(options) {
    this.mode = options.mode || 'hash'
    this.routes = options.routes || []
    this.routesMap = this.createMap(this.routes); // 维护状态
    this.history = new HistoryRoute();

    this.init();
  }

  init() {
    if (this.mode === 'hash') {
      // 先判断用户打开时有没有hash值,没有的话跳转到#/
      location.hash ? '' : (location.hash = '/');
      window.addEventListener('load', () => {
        this.history.current = location.hash.slice(1);
      })
      window.addEventListener('hashchange', () => {
        this.history.current = location.hash.slice(1);
      })
    } else { // browserRouter
      loaction.pathname ? '' : (location.pathname = '/');
      window.addEventListener('load', () => {
        this.history.current = location.pathname;
      })
      window.addEventListener('popstate', () => {
        this.history.current = location.pathname;
      })
    }
  }

  createMap(routes) { // 将component和path匹配
    return routes.reduce((pre, cur) => {
      pre[cur.path] = cur.component; // 用一个对象,以键值对的方式将component和path存储起来
      return pre;
    }, {})
    // 返回的形式如下
    // {
    //   ['/home']: Home,
    //   ['/about']: About
    // }
  }
}
// 作为插件安装,就是vue.use(router)这步的操作
VueRouter.install = (v) => {
  Vue = v;
  Vue.mixin({
    beforeCreate() {
      if (this.$options && this.$options.router) {
        // 如果是根组件
        this._root = this; //把当前实例挂载到_root上
        this._router = this.$options.router;
        Vue.util.defineReactive(this, 'xxx', this._router.history);
      } else {
        //如果是子组件
        this._root = this.$parent && this.$parent._root;
      }
      Object.defineProperty(this, '$router', {
        get() {
          return this._root._router;
        },
      });
      Object.defineProperty(this, '$route', {
        get() {
          return this._root._router.history.current;
        },
      });
    },
  });
  Vue.component('router-link', {
    props: {
      to: String,
    },
    render(h) {
      let mode = this._self._root._router.mode;
      let to = mode === 'hash' ? '#' + this.to : this.to;
      return h('a', { attrs: { href: to } }, this.$slots.default);
    },
  });
  Vue.component('router-view', {
    render(h) {
      let current = this._self._root._router.history.current;
      let routeMap = this._self._root._router.routesMap;
      return h(routeMap[current]);
    },
  });
}

React Router

react-router原理

先来看基本的使用,可以看出router是类似于ProviderContext的挂载。

// App.tsx
import { Route, Routes, Link } from 'react-router-dom'
import Home from './views/Home'
import About from './views/About'
import './App.css'

function App() {
  return (
    <>
      <h1>React Router Demo</h1>
      <Link to="/">Home</Link>
      <Link to="about">About</Link >
      <br />
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="about" element={<About />} />
      </Routes>
    </>
  )
}

export default App;

// main.tsx
import React from 'react'
import ReactDOM from 'react-dom/client'
import { BrowserRouter } from 'react-router-dom'
import App from './App.tsx'
import './index.css'

ReactDOM.createRoot(document.getElementById('root')!).render(
  <React.StrictMode>
    <BrowserRouter>
      <App />
    </BrowserRouter>
  </React.StrictMode>
)

我们去看<Routes>组件的声明,重点是去看createRoutesFromChildren

export function Routes({
  children,
  location,
}: RoutesProps): React.ReactElement | null {
  return useRoutes(createRoutesFromChildren(children), location);
}

可以看到对其中的每个子节点,也就是<Route>组件进行了递归的处理,生成route,其中children属性包含了嵌套的子组件,最终返回一个routes数组。

export function createRoutesFromChildren(
  children: React.ReactNode,
  parentPath: number[] = []
): RouteObject[] {
  let routes: RouteObject[] = [];

  React.Children.forEach(children, (element, index) => {
    let treePath = [...parentPath, index];

    if (element.type === React.Fragment) {
      routes.push.apply(
        routes,
        createRoutesFromChildren(element.props.children, treePath)
      );
      return;
    }

    let route: RouteObject = {...};

    if (element.props.children) {
      route.children = createRoutesFromChildren(
        element.props.children,
        treePath
      );
    }

    routes.push(route);
  });

  return routes;
}

这样我们就知道了RoutesRoute组件是怎么实现嵌套路由的。

接着我们以BrowserRouter为例,去看在App外嵌套BrowserRouter做了什么。

export function BrowserRouter({
  basename,
  children,
  future,
  window,
}: BrowserRouterProps) {
  let historyRef = React.useRef<BrowserHistory>();
  if (historyRef.current == null) {
    historyRef.current = createBrowserHistory({ window, v5Compat: true });
  }

  let history = historyRef.current;
  let [state, setStateImpl] = React.useState({
    action: history.action,
    location: history.location,
  });
  let { v7_startTransition } = future || {};
  let setState = React.useCallback(
    (newState: { action: NavigationType; location: Location }) => {
      v7_startTransition && startTransitionImpl
        ? startTransitionImpl(() => setStateImpl(newState))
        : setStateImpl(newState);
    },
    [setStateImpl, v7_startTransition]
  );

  React.useLayoutEffect(() => history.listen(setState), [history, setState]);

  return (
    <Router
      basename={basename}
      children={children}
      location={state.location}
      navigationType={state.action}
      navigator={history}
      future={future}
    />
  );
}

可以看到重点有两块,一个是创建了history对象维护路由的上下文,第二个是用useLayoutEffect监听history的变化进行更新,最后返回Router组件,所以我们要去看createBrowserHistoryRouter组件做了什么。

createBrowserHistory

这里通过取出location中的pathname,search,hash,调用getUrlBasedHistory,生成history对象。

export function createBrowserHistory(
  options: BrowserHistoryOptions = {}
): BrowserHistory {
  function createBrowserLocation(
    window: Window,
    globalHistory: Window["history"]
  ) {
    let { pathname, search, hash } = window.location;
    return createLocation(
      "",
      { pathname, search, hash },
      (globalHistory.state && globalHistory.state.usr) || null,
      (globalHistory.state && globalHistory.state.key) || "default"
    );
  }

  function createBrowserHref(window: Window, to: To) {
    return typeof to === "string" ? to : createPath(to);
  }

  return getUrlBasedHistory(
    createBrowserLocation,
    createBrowserHref,
    null,
    options
  );
}

history对象包含了一些对URL的操作,包括push,pop,replace等。

function getUrlBasedHistory(...): UrlHistory {
  let globalHistory = window.history;
  let action = Action.Pop;
  let listener: Listener | null = null;
  function getIndex(): number {
    let state = globalHistory.state || { idx: null };
    return state.idx;
  }
  function handlePop() {
    action = Action.Pop;
    let nextIndex = getIndex();
    let delta = nextIndex == null ? null : nextIndex - index;
    index = nextIndex;
    if (listener) {
      listener({ action, location: history.location, delta });
    }
  }

  function push(to: To, state?: any) {
    action = Action.Push;
    let location = createLocation(history.location, to, state);
    if (validateLocation) validateLocation(location, to);

    index = getIndex() + 1;
    let historyState = getHistoryState(location, index);
    let url = history.createHref(location);

    globalHistory.pushState(historyState, "", url);
  }

  function replace(to: To, state?: any) {
    action = Action.Replace;
    let location = createLocation(history.location, to, state);
    if (validateLocation) validateLocation(location, to);

    index = getIndex();
    let historyState = getHistoryState(location, index);
    let url = history.createHref(location);
    globalHistory.replaceState(historyState, "", url);
  }

  function createURL(to: To): URL {
    let base =
      window.location.origin !== "null"
        ? window.location.origin
        : window.location.href;

    let href = typeof to === "string" ? to : createPath(to);
    return new URL(href, base);
  }

  let history: History = {
    get action() {
      return action;
    },
    get location() {
      return getLocation(window, globalHistory);
    },
    listen(fn: Listener) {
      window.addEventListener(PopStateEventType, handlePop);
      listener = fn;
      return () => {
        window.removeEventListener(PopStateEventType, handlePop);
        listener = null;
      };
    },
    createHref(to) {
      return createHref(window, to);
    },
    createURL,
    encodeLocation(to) {
      let url = createURL(to);
      return {
        pathname: url.pathname,
        search: url.search,
        hash: url.hash,
      };
    },
    push,
    replace,
    go(n) {
      return globalHistory.go(n);
    },
  };

  return history;
}

Router

这里可以看到,它就是一个Provider包裹App,使我们应用内部能够获取到Router

export function Router(...): React.ReactElement | null {
  let basename = basenameProp.replace(/^\/*/, "/");
  let navigationContext = React.useMemo(
    () => ({
      basename,
      navigator,
      static: staticProp,
      future: {
        v7_relativeSplatPath: false,
        ...future,
      },
    }),
    [basename, future, navigator, staticProp]
  );

  if (typeof locationProp === "string") {
    locationProp = parsePath(locationProp);
  }

  let {
    pathname = "/",
    search = "",
    hash = "",
    state = null,
    key = "default",
  } = locationProp;

  let locationContext = React.useMemo(() => {
    let trailingPathname = stripBasename(pathname, basename);

    return {
      location: {
        pathname: trailingPathname,
        search,
        hash,
        state,
        key,
      },
      navigationType,
    };
  }, [basename, pathname, search, hash, state, key, navigationType]);

  return (
    <NavigationContext.Provider value={navigationContext}>
      <LocationContext.Provider children={children} value={locationContext} />
    </NavigationContext.Provider>
  );
}

手写实现

BrowserRouter

首先,我们要创建Router的上下文,再获取location中的pathname,用状态去存储。

function BrowserRouter(props) {
  const RouterContext = createContext();

  const [path, setPath] = useState(() => {
    const { pathname } = window.location;
    return pathname || '/';
  })
}

接下去要监听用户的前进后退行为,即触发popstate事件,将path更新为最新的pathname

useEffect(() => {
  window.addEventListener('popstate', handlePopstate);
  return window.removeEventListener('popstate', handlePopstate);
}, []);
const handlePopstate = () => {
  const { pathname } = window.location;
  setPath(pathname);
}

最后返回我们的Provider

return (
    <RouterContext.Provider value={path}>
      {props.children}
    </RouterContext.Provider>
  )

此时我们还缺少一些跳转的功能:

const push = (path) => {
  setPath(path);
  window.history.pushState({ path }, null, path)
};
const goBack = () => {
  window.history.go(-1);
}

所以同样要创建HistoryContext并且包裹children

const HistoryContext = createContext();
return (
  <RouterContext.Provider value={path}>
    <HistoryContext.Provider value={{ push, goBack }}>
      {props.children}
    </HistoryContext.Provider>
  </RouterContext.Provider>
)

然后要考虑怎么去消费我们的Router,就是通过Routes中包裹的Route,判断当前path是否等于componentPath,如果相等就渲染当前组件。

export function Route(props) {
  const { element: Component, path: componentPath } = props;
  return (
    <RouterContext.Consumer>
      {(path) => {
        componentPath === path ? <Component /> : null;
      }}
    </RouterContext.Consumer>
  )
}

最终代码如下:

// browser-router
function BrowserRouter(props) {
  const RouterContext = createContext();

  const HistoryContext = createContext();

  const [path, setPath] = useState(() => {
    const { pathname } = window.location;
    return pathname || '/';
  });

  useEffect(() => {
    window.addEventListener('popstate', handlePopstate);

    return window.removeEventListener('popstate', handlePopstate);
  }, []);

  const handlePopstate = () => {
    const { pathname } = window.location;
    setPath(pathname);
  }

  const push = (path) => {
    setPath(path);
    window.history.pushState({ path }, null, path)
  };

  const goBack = () => {
    window.history.go(-1);
  }
  return (
    <RouterContext.Provider value={path}>
      <HistoryContext.Provider value={{ push, goBack }}>
        {props.children}
      </HistoryContext.Provider>
    </RouterContext.Provider>
  )
}

export default BrowserRouter

export function Route(props) {
  const { element: Component, path: componentPath } = props;
  return (
    <RouterContext.Consumer>
      {(path) => {
        componentPath === path ? <Component /> : null;
      }}
    </RouterContext.Consumer>
  )
}

HashRouter

区别就是获取的是location.hash,监听的是hashChange事件,改变的是location.hash

const [path, setPath] = useState(() => {
  const { hash } = window.location;
  if (hash) {
    return hash.slice(1)
  }
  return '/#/'
});
useEffect(() => {
  window.addEventListener('hashChange', handleHashChange);
  return window.removeEventListener('hashChange', handleHashChange);
}, []);
const handleHashChange = () => {
  const { hash } = window.location;
  setPath(hash.slice(1));
}
const push = (path) => {
  setPath(path);
  window.location.hash = path;
};

  • 43
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Vue RouterVue.js 官方的路由管理器。它实现前端路由的核心功能,允许开发者在 Vue.js 单页面应用中进行页面的跳转和导航。 Vue Router实现原理可以简单概括为以下几个方面: 1. 声明式路由配置:开发者通过定义一组路由规则来描述不同路径下应该展示哪个组件。这些路由规则可以通过路由配置文件或者直接在组件中定义。Vue Router 会将这些规则解析成一个路由映射表,用于后续的路由匹配。 2. 路由匹配:当用户访问一个特定路径时,Vue Router 会根据路由映射表进行匹配,找出对应的组件。匹配过程中会考虑路由的嵌套关系和参数传递。 3. 导航守卫:Vue Router 提供了一套导航守卫机制,允许开发者在路由切换前后执行自定义的逻辑。导航守卫可以用于实现登录验证、权限控制、路由切换动画等功能。 4. 响应式数据变化:Vue Router 使用 Vue.js 的响应式系统来跟踪当前路由状态的变化。当路由切换时,相关的组件会根据新的路由状态重新渲染。 5. 历史管理:Vue Router 使用浏览器的 History API(或者 hash 模式)来管理浏览器历史记录。它提供了一些方法来实现前进、后退、跳转等操作,同时保持路由状态的同步。 总的来说,Vue Router 实现原理是基于路由映射表和响应式系统,通过匹配路径和执行导航守卫来实现页面的跳转和导航。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值