🦍

SPAをJavaScriptのみで実装してみた

2022/11/27に公開

SPA とは

SPA とは、Single Page Application の略で、1 つのページでアプリケーションを実装することを指します。
モダンな JS フレームワークを使っている人は、なじみ深いと思います。

従来の MPA(Multi Page Application)では、ページ遷移するたびにサーバーから HTML を取得し、ブラウザに描画していました。

それに対し、SPA が実装されたページは、Javascript で HTML の一部を差し替えて必要な部分だけを読み込みます。

Vue.js での例

<!DOCTYPE html>
<html lang="">
  <body>
    <noscript>
      <strong>JSが存在しない時の処理</strong>
    </noscript>
    <div id="app"></div>
    <!-- built files will be auto injected -->
  </body>
</html>

Vue.js で SPA を実装する場合、HTML には <div id="app"></div> という要素が存在し、その中に Vue が解析した html が差し込まれていってアプリケーションが動作します。

この処理は基本的にVue.js,React,Angular,などの JS フレームワーク/ライブラリが行っているので、実装者はその作法に従って実装すれば、容易に SPA のアプリケーションが作られます。

今回はこの一連の処理を、JavaScript のみで実装してみたいと思います。

JavaScript のみで SPA を実装する

フォルダ構成

  • index.html
  • js
    • router.js
  • pages
    • index.html
    • about.html
    • profile.html
    • 404.html

このルート直下にindex.htmlというファイルが初めに読み込まれる単一の HTML ファイルです。
pages配下の HTML ファイルは JS によって index.html に差し込まれます。

index.html の実装

ルート直下の index.html

<!DOCTYPE html>
<html lang="ja">
  <body>
    <div>
      <nav>
        <a href="/" onclick="route()">Home</a>
        <a href="/about/" onclick="route()">About</a>
        <a href="/profile/" onclick="route()">profile</a>
      </nav>
      <hr />
      <div id="main-page"></div>
    </div>
    <script src="js/router.js"></script>
  </body>
</html>

anchor タグのhref属性には、ページ遷移先の URL を指定します。
onclick="route()"は後ほど実装するroute()関数を呼び出すためのものです。
JS を実装するときに説明します。

<div id="main-page"></div>ここに、JavaScript によって取得された HTML が差し込まれます。

上のVue.jsの例で言うところの<div id="app"></div>と同じです。

ちなにみ、この状態で、AboutProfileボタンをクリックすると、エラーが発生します。
指定したパスに html は存在しませんので。

route.js の実装

"use strict";

const route = (event) => {
  event = event || window.event;
  event.preventDefault();
  window.history.pushState({}, "", event.target.href);
  handleLocation();
};

const routes = {
  404: "/pages/404.html",
  "/": "/pages/index.html",
  "/about/": "/pages/about.html",
  "/profile/": "/pages/profile.html",
};

const handleLocation = async () => {
  const path = window.location.pathname;
  const route = routes[path] || routes[404];
  const html = await fetch(route).then((data) => data.text());
  document.getElementById("main-page").innerHTML = html;
};

window.addEventListener("popstate", (event) => {
  handleLocation();
});

handleLocation();

一つずつ、順を追ってみていきます。
このrouter.jsが読み込まれて、一番初めに実行されるのかhandleLocationです。

const handleLocation = async () => {
  // 現在のパスを取得
  const path = window.location.pathname;
  // 現在のパスに紐ずくhtmlのパスを取得
  const route = routes[path] || routes[404];
  // htmlを取得
  const html = await fetch(route).then((data) => data.text());
  // 取得したhtmlを動的にルート直下のindex.htmlに差し込む
  document.getElementById("main-page").innerHTML = html;
};

上記の流れで、JavaScript によって、html が描画されます。

次に anchor タグがクリックされた時の挙動です。

const route = (event) => {
  // クリック時のイベントを取得
  event = event || window.event;
  // ブラウザのデフォルト動作をキャンセル
  event.preventDefault();
  // ブラウザの履歴に追加
  window.history.pushState({}, "", event.target.href);
  handleLocation();
};

この関数が行なっていることは、history API を使って、ブラウザの履歴に追加することです。
history API については、こちらを参照してください。

history API については詳しい説明は、省きますが、例えば、aboutボタンをクリックした時に、window.history.pushState({}, "", event.target.href);を実行後
console.log(window.location.pathname);を実行すると、/about/が出力されます。

window.history.pushStateによって、ブラウザの履歴にどんどん追加されていっていると考えると、わかりやすいです。
ボタンを押すたびに、window.history.pushStateが実行されて、ブラウザの履歴に追加されるので、10 回ボタンを押すと、10 個の履歴が追加されます。

最後に、popstate についてです。

window.addEventListener("popstate", () => { handleLocation(); });

popstateは、ブラウザの戻るボタンや進むボタンが押された時に発火します。
さきほどのwindow.history.pushStateで、ブラウザの履歴に追加しているので、ブラウザの戻るボタンを押すと、一回前の履歴に戻ることができます。

戻った時に、popstateが発火され、ブラウザの history を元に紐づく html を handleLocation()で取得してきて、差し込んでいます。

これで戻るや進むボタンが使えるようになりました。

pages 配下の html

中身は好きなように用意してください。

404

<div>
  <h1>404 Not Found</h1>
  <p>Oh no! It looks like the page you're trying to get to is missing!</p>
</div>

about

<div>
  <h1>About</h1>
  <p>I am gonne introduce about Me</p>
</div>

index

<div>
  <h1>index</h1>
  <p>This is an indexPage</p>
</div>

profile

<div>
  <h1>Profile</h1>
  <p>My name is JavaScript</p>
</div>

まとめ

javascript のみで、SPA を実装してみました。
普段使っているフレームワークを実装してみると、興味深いですね。

参考

https://github.com/mitchwadair/vanilla-spa-router

GitHubで編集を提案

Discussion