フロントエンドルーティング原理の解析と実現

25993 ワード

原文リンク:github.com/whinc/blog/…
このように1ページのアプリケーションが流行している今日、驚くべきフロントエンドルーティングはすでに各フレームワークの基礎的な標準となり、各フレームワークは強力なルーティング機能を提供し、ルーティングの実現が複雑になっている.ルーティングの内部実装を理解するのは難しいが,ルーティング実装の基本原理だけを理解するのは簡単である.本稿では,フロントエンドルーティングの主流の実装方式hashとhistoryについて,オリジナルJS/React/Vueの計6バージョンを参考に,各バージョンの実装コードは約25~40行程度(空行を含む)提供する.
フロントエンドルーティングとは?
ルーティングの概念は、URLと処理関数とのマッピング関係を記述するサービス側に由来する.
Webフロントエンド単一ページアプリケーションSPA(Single Page Application)では、ルーティングは、ページをリフレッシュすることなく、URLとUIとのマッピング関係を記述する.
フロントエンドルーティングを実現するにはどうすればいいですか?
フロントエンドルーティングを実現するには、2つのコアを解決する必要があります.
  • URLを変更してページのリフレッシュを起こさないにはどうすればいいですか?
  • URLの変化をどのように検出しますか?

  • 以下では,hashとhistoryの2つの実装方式を用いて,上記の2つのコア問題に答える.
    hash実装
  • hashはURL中のhash(#)および後の部分であり、アンカーとしてページ内をナビゲートすることがよく用いられ、URL中のhash部分を変更するとページリフレッシュ
  • を引き起こすことはない.
  • hashchangeイベントによってURLの変化を傍受し、URLを変更する方法は、ブラウザの前進後退によってURLを変更し、タグによってURLを変更し、window.locationによってURLを変更する、これらの状況によってURLを変更するとhashchangeイベント
  • がトリガーされる.
    history実装
  • historyはpushStateとreplaceStateの2つの方法を提供し、この2つの方法はURLのpath部分を変更してページリフレッシュ
  • を引き起こさない.
  • historyではhashchangeイベントのようなpopstateイベントが提供されていますが、popstateイベントにはブラウザが前進後退してURLを変更するとpopstateイベントがトリガーされ、pushState/replaceStateまたはラベルでURLを変更してもpopstateイベントはトリガーされません.幸いなことに、pushState/replaceStateの呼び出しとラベルのクリックイベントをブロックしてURLの変化を検出することができるので、リスニングURLの変化は実現できますが、hashchangeほど便利ではありません.

  • オリジナルJS版フロントエンドルーティング実現
    前節で説明した2つの実装方式に基づいて,hashバージョンとhistoryバージョンのルーティングをそれぞれ実装し,例はフレームワークに依存せずにオリジナルHTML/JS実装を用いた.
    hashベースの実装
    実行効果:
    HTMLセクション:
    <body>
      <ul>
        
        <li><a href="#/home">homea>li>
        <li><a href="#/about">abouta>li>
    
        
        <div id="routeView">div>
      ul>
    body>
    

    JavaScriptセクション:
    //           hashchange,         hashchange   
    window.addEventListener('DOMContentLoaded', onLoad)
    //       
    window.addEventListener('hashchange', onHashChange)
    
    //     
    var routerView = null
    
    function onLoad () {
      routerView = document.querySelector('#routeView')
      onHashChange()
    }
    
    //      ,         UI
    function onHashChange () {
      switch (location.hash) {
        case '#/home':
          routerView.innerHTML = 'Home'
          return
        case '#/about':
          routerView.innerHTML = 'About'
          return
        default:
          return
      }
    }
    

    historyベースの実装
    実行効果:
    HTMLセクション:
    <body>
      <ul>
        <li><a href='/home'>homea>li>
        <li><a href='/about'>abouta>li>
    
        <div id="routeView">div>
      ul>
    body>
    

    JavaScriptセクション:
    //           hashchange,         hashchange   
    window.addEventListener('DOMContentLoaded', onLoad)
    //       
    window.addEventListener('popstate', onPopState)
    
    //     
    var routerView = null
    
    function onLoad () {
      routerView = document.querySelector('#routeView')
      onPopState()
    
      //    ラベルはイベントのデフォルト  をクリックし、クリック にpushStateを  してURLを  し、  UIを  することで、リンクをクリックしてURLとUIを  する  を  します。
      var linkList = document.querySelectorAll('a[href]')
      linkList.forEach(el => el.addEventListener('click', function (e) {
        e.preventDefault()
        history.pushState(null, '', el.getAttribute('href'))
        onPopState()
      }))
    }
    
    //      ,         UI
    function onPopState () {
      switch (location.pathname) {
        case '/home':
          routerView.innerHTML = 'Home'
          return
        case '/about':
          routerView.innerHTML = 'About'
          return
        default:
          return
      }
    }
    

    React版フロントエンドルーティング実装
    hashベースの実装
    実行効果:
    使用方法はreact-routerと似ています.
      
        <ul>
          <li>
            <Link to="/home">homeLink>
          li>
          <li>
            <Link to="/about">aboutLink>
          li>
        ul>
    
        "/home" render={() => <h2>Homeh2>} />
        <Route path="/about" render={() => <h2>Abouth2>} />
      BrowserRouter>
    

    BrowserRouter実装
    export default class BrowserRouter extends React.Component {
      state = {
        currentPath: utils.extractHashPath(window.location.href)
      };
    
      onHashChange = e => {
        const currentPath = utils.extractHashPath(e.newURL);
        console.log("onHashChange:", currentPath);
        this.setState({ currentPath });
      };
    
      componentDidMount() {
        window.addEventListener("hashchange", this.onHashChange);
      }
    
      componentWillUnmount() {
        window.removeEventListener("hashchange", this.onHashChange);
      }
    
      render() {
        return (
          <RouteContext.Provider value={{currentPath: this.state.currentPath}}>
            {this.props.children}
          RouteContext.Provider>
        );
      }
    }
    

    Route実装
    export default ({ path, render }) => (
      <RouteContext.Consumer>
        {({currentPath}) => currentPath === path && render()}
      RouteContext.Consumer>
    );
    

    Link実装
    export default ({ to, ...props }) => <a {...props} href={"#" + to} />;
    

    historyベースの実装
    実行効果:
    使用方法はreact-routerと似ています.
      
        <ul>
          <li>
            <Link to="/home">homeLink>
          li>
          <li>
            <Link to="/about">aboutLink>
          li>
        ul>
    
        "/home" render={() => <h2>Homeh2>} />
        <Route path="/about" render={() => <h2>Abouth2>} />
      HistoryRouter>
    

    HistoryRouter実装
    export default class HistoryRouter extends React.Component {
      state = {
        currentPath: utils.extractUrlPath(window.location.href)
      };
    
      onPopState = e => {
        const currentPath = utils.extractUrlPath(window.location.href);
        console.log("onPopState:", currentPath);
        this.setState({ currentPath });
      };
    
      componentDidMount() {
        window.addEventListener("popstate", this.onPopState);
      }
    
      componentWillUnmount() {
        window.removeEventListener("popstate", this.onPopState);
      }
    
      render() {
        return (
          <RouteContext.Provider value={{currentPath: this.state.currentPath, onPopState: this.onPopState}}>
            {this.props.children}
          RouteContext.Provider>
        );
      }
    }
    

    Route実装
    export default ({ path, render }) => (
      <RouteContext.Consumer>
        {({currentPath}) => currentPath === path && render()}
      RouteContext.Consumer>
    );
    

    Link実装
    export default ({ to, ...props }) => (
      
        {({ onPopState }) => (
          {
    e.preventDefault();
    window.history.pushState(null, "", to);
    onPopState();
    }}
    />
    )}
    );
    

    Vueバージョンフロントエンドルーティング実装
    hashベースの実装
    実行効果:
    使用方法はvue-routerと似ています(vue-routerはプラグインメカニズムを介してルーティングに注入されますが、実装の詳細は隠されています.コードを直感的に維持するために、ここではVueプラグインパッケージは使用されません):
        <div>
          <ul>
            <li><router-link to="/home">homerouter-link>li>
            <li><router-link to="/about">aboutrouter-link>li>
          ul>
          <router-view>router-view>
        div>
    
    const routes = {
      '/home': {
        template: '

    Home

    '
    }, '/about': { template: '

    About

    '
    } } const app = new Vue({ el: '.vue.hash', components: { 'router-view': RouterView, 'router-link': RouterLink }, beforeCreate () { this.$routes = routes } })

    router-view実装
    
    
    
    import utils from '~/utils.js'
    export default {
      data () {
        return {
          routeView: null
        }
      },
      created () {
        this.boundHashChange = this.onHashChange.bind(this)
      },
      beforeMount () {
        window.addEventListener('hashchange', this.boundHashChange)
      },
      mounted () {
        this.onHashChange()
      },
      beforeDestroy() {
        window.removeEventListener('hashchange', this.boundHashChange)
      },
      methods: {
        onHashChange () {
          const path = utils.extractHashPath(window.location.href)
          this.routeView = this.$root.$routes[path] || null
          console.log('vue:hashchange:', path)
        }
      }
    }
    
    

    router-link実装
    
    
    
    export default {
      props: {
        to: String
      },
      methods: {
        onClick () {
          window.location.hash = '#' + this.to
        }
      }
    }
    
    

    historyベースの実装
    実行効果:
    使用方法はvue-routerと似ています.
        <div>
          <ul>
            <li><router-link to="/home">homerouter-link>li>
            <li><router-link to="/about">aboutrouter-link>li>
          ul>
          <router-view>router-view>
        div>
    
    const routes = {
      '/home': {
        template: '

    Home

    '
    }, '/about': { template: '

    About

    '
    } } const app = new Vue({ el: '.vue.history', components: { 'router-view': RouterView, 'router-link': RouterLink }, created () { this.$routes = routes this.boundPopState = this.onPopState.bind(this) }, beforeMount () { window.addEventListener('popstate', this.boundPopState) }, beforeDestroy () { window.removeEventListener('popstate', this.boundPopState) }, methods: { onPopState (...args) { this.$emit('popstate', ...args) } } })

    router-view実装:
    
    
    
    import utils from '~/utils.js'
    export default {
      data () {
        return {
          routeView: null
        }
      },
      created () {
        this.boundPopState = this.onPopState.bind(this)
      },
      beforeMount () {
        this.$root.$on('popstate', this.boundPopState)
      },
      beforeDestroy() {
        this.$root.$off('popstate', this.boundPopState)
      },
      methods: {
        onPopState (e) {
          const path = utils.extractUrlPath(window.location.href)
          this.routeView = this.$root.$routes[path] || null
          console.log('[Vue] popstate:', path)
        }
      }
    }
    
    

    router-link実装
    
    
    
    export default {
      props: {
        to: String
      },
      methods: {
        onClick () {
          history.pushState(null, '', this.to)
          this.$root.$emit('popstate')
        }
      }
    }
    
    

    小結
    フロントエンドルーティングのコア実装原理は簡単であるが、特定のフレームワークと組み合わせると、ダイナミックルーティング、ルーティングパラメータ、ルーティングアニメーションなど、フレームワークに多くの特性が追加され、ルーティング実装が複雑になる.本稿では、フロントエンドルーティングの最もコア部分の実装のみを分析し、hashとhistoryの2つのモードに基づいて、それぞれオリジナルJS/React/Vueの3つの実装を提供し、合計6つの実装バージョンを参考にして、あなたの役に立つことを望んでいます.
    すべての例のコードはGithub倉庫:githubに置かれています.com/whinc/web-r…
    リファレンス
  • 単一ページルーティングのいくつかの実装原理
  • を詳細に説明する
  • 単ページ応用ルーティング実現原理:React-Routerを例に
  • .