Published on

聊聊前端的路由方案

Authors
  • avatar
    Name
    noodles
    每个人的花期不同,不必在乎别人比你提前拥有

概要

本文主要梳理前端路由的实现方案,按照如下的逻辑进行梳理: 前置知识 => 路由方案现状 => 从源码的解读理解前端路由的实现过程

前置知识

history

方法(属性)含义
history.length只读, 代表当前会话历史的长度
history.state只读, 代表当前会话栈顶的state
history.go(number) history.forward() history.back()从当前会话加载特定的页面,会触发popstate事件
pushState(state, title, url)在当前会话的添加一个新的记录(关联state) url参数需要保证同源策略
replaceState(state, title, url)替换当前会话栈顶的记录(不会增加history长度,关联state) url参数需要保证同源策略

history相关事件

当用户触发浏览器动作或者js调用history.back/history.forward/history.go方法时,会触发popstate事件。

hash相关事件

  1. 当url片段标识符改变(#xxx), 会触发hashchange事件。
  2. 当设置与当前不同的hash片段的时候,会在当前会话中添加一个新的记录。

路由方案现状

方案原理优缺点
基于history实现的路由方案使用history相关事件和方法完成路由的切换history可以设置同源下的任意url,需要注意与服务端结合的场景,防止出现404
基于hash实现的路由方案使用hash相关事件完成路由的切换hash只能改变当前url的#,有局限性

从源码的了解路由的实现过程

以下源码分析了history路由的实现过程,源码涉及history, react-router.整体的实现逻辑如下

route

下面代码是在react项目中使用history路由实现的一个例子,它能实现根据特定的path来渲染对应的组件。

import './App.css'
import React from 'react'
import { BrowserRouter as Router, Switch, Route } from 'react-router-dom'
function App() {
  return (
    <Router>
      <Switch>
        <Route exact path="/">
          <div>home</div>
        </Route>
        <Route path="/about">
          <div>about</div>
        </Route>
        <Route path="/dashboard">
          <div>dashboard</div>
        </Route>
      </Switch>
    </Router>
  )
}
export default App

react-router/packages/react-router-dom/modules/BrowserRouter.js

import React from 'react'
import { Router } from 'react-router'
import { createBrowserHistory as createHistory } from 'history'

class BrowserRouter extends React.Component {
  history = createHistory(this.props)

  render() {
    // 初始化browser history 可以推断出路由的切换逻辑是history与Router结合的实现
    return <Router history={this.history} children={this.props.children} />
  }
}
export default BrowserRouter

packages/index.ts

    // 在最后执行跳转的时候 会执行所有的listen函数
    function applyTx(nextAction: Action) {
      action = nextAction;
      [index, location] = getIndexAndLocation();
      listeners.call({ action, location });
    }
    let history: BrowserHistory = {
      // 以下为主要的跳转函数,在实现跳转逻辑的时候都调用了applyT方法。
      push,
      replace,
      go,
      back() {
        go(-1);
      },
      forward() {
        go(1);
      },
      // listion方法用于增加路由切换的监听函数
      listen(listener) {
        return listeners.push(listener);
      },
      // block方法允许传入一个block函数,在路由跳转的时候会执行所有的blocker函数
      block(blocker) {
        let unblock = blockers.push(blocker);
        if (blockers.length === 1) {
          window.addEventListener(BeforeUnloadEventType, promptBeforeUnload);
        }
        return function() {
          unblock();
          if (!blockers.length) {
            window.removeEventListener(BeforeUnloadEventType, promptBeforeUnload);
          }
        };
      }
    };
    return history;

从上面history源码看出,history这个库主要是维护history的相关状态(state, location, hash)并且增加路由跳转的告知能力.

react-router/packages/react-router/modules/Router.js

import React from 'react'
import HistoryContext from './HistoryContext.js'
import RouterContext from './RouterContext.js'
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._isMounted = false
    this._pendingLocation = null
    if (!props.staticContext) {
      // 这里订阅了history的变化并且在变化的之后更新location
      this.unlisten = props.history.listen((location) => {
        if (this._isMounted) {
          this.setState({ location })
        } else {
          this._pendingLocation = location
        }
      })
    }
  }
  componentDidMount() {
    this._isMounted = true
    if (this._pendingLocation) {
      this.setState({ location: this._pendingLocation })
    }
  }
  render() {
    return (
      // 将location作为context 在需要订阅的位置获取   Route消费location完成特定children的渲染。
      <RouterContext.Provider
        value={{
          history: this.props.history,
          location: this.state.location,
          match: Router.computeRootMatch(this.state.location.pathname),
          staticContext: this.props.staticContext,
        }}
      >
        <HistoryContext.Provider
          children={this.props.children || null}
          value={this.props.history}
        />
      </RouterContext.Provider>
    )
  }
}
export default Router