60 行の JavaScript ルーター


私は自分で Web アプリケーションを構築していましたが、NPM パッケージと JS フレームワークが大きく複雑になっているため、JS フレームワークをインストールせず、今回はゼロからアプリを構築することにしました.

新しい Web アプリを作成するには、Router がページの変更を処理する必要があります.これは私の試みです.

では、ルーターは Web アプリケーションに対して実際に何をするのでしょうか.
  • アプリは、開いている URL を読み取り、必要なコンテンツを表示できる必要があります.たとえば、ページ www.mybook.com/user/1 を開くと、ページはユーザー 1 の情報をレンダリングする必要があります.
  • ページは URL の変更をリッスンする必要があるため、ユーザーを www.mybook.com/post/my-latest-news にリダイレクトするボタンまたは画像をクリックすると、ページは更新されず、代わりに古いコンテンツが削除され、新しい必要なコンテンツがレンダリングされます.コンテンツをレンダリングするこの方法は、通常、シングル ページ アプリケーションまたは SPA と呼ばれます.
  • ページには URL 履歴メモリが必要なので、ブラウザで戻るボタンまたは進むボタンを押したときに、アプリケーションは表示するページを認識する必要があります.
  • ルーターがルートを定義し、ユーザーがそのルートに到達したときに何らかのアクションを実行できるようにしたいと考えています.

  • 例えば

    router.on("/post/my-latest-news", (params) => {
      // In here, I remove old content and render new one 
    })
    


  • また、ルーターが URL のパラメーターを受け入れるようにしたいと考えています.

  • たとえば、"/post/:id" は、どの投稿を表示するかを決定するときに、パラメーターとして id 値を提供します.

    それが基本だと思います.

    ルート変更のリッスンには、popstate listener API を使用します.

    URL履歴には、ブラウザHistory APIを使用します


    JavaScript の実装



    このルーターのコードは Github にあります.

    class Router {
        constructor() {
            this.routes = new Map();
            this.current = [];
    
            // Listen to the route changes, and fire routeUpdate when route change happens.
            window.onpopstate = this.routeUpdate.bind(this);
        }
    
        // Returns the path in an array, for example URL "/blog/post/1" , will be returned as ["blog", "post", "1"]
        get path() {
            return window.location.pathname.split('/').filter((x) => x != '');
        }
    
        // Returns the pages query parameters as an object, for example "/post/?id=2" will return { id:2 } 
        get query() {
            return Object.fromEntries(new URLSearchParams(window.location.search));
        }
    
        routeUpdate() {
            // Get path as an array and query parameters as an object
            const path = this.path;
            const query = this.query;
    
            // When URL has no path, fire the action under "/" listener and return 
            if (path.length == 0) {
                this.routes.get('/')(path);
                return;
            }
    
            // When same route is already active, don't render it again, may cause harmful loops.
            if (this.current.join() === path.join()) return;
    
            // Set active value of current page
            this.current = path;
    
            // Here I save the parameters of the URL, for example "/post/:page", will save value of page
            let parameters = {};
    
            // Loop though the saved route callbacks, and find the correct action for currect URL change
            for (let [route, callback] of this.routes) {
    
                // Split the route action name into array
                const routes = route.split('/').filter((x) => x != '');
                const matches = routes
                    .map((url, index) => {
                        // When the route accepts value as wildcard accept any value
                        if (url == '*') return true;
    
                        // Route has a parameter value, because it uses : lets get that value from the URL
                        if (url.includes(':')) {
                            parameters[url.split(':')[1]] = path[index];
                            return true;
                        }
                        // The new URL matches the saved route callback url, return true, meaning the action should be activated.
                        if (url == path[index]) return true;
                        return false;
                    })
                    .filter((x) => x);
    
                // When the router has found that current URL, is matching the saved route name, fire the callback action with parameters included 
                if (matches.length == routes.length && routes.length > 0) {
                    callback({ path, parameters, query });
                }
            }
        }
    
        // Listen for route changes, required route name and the callback function, when route matches.
        on(route, callback) {
            this.routes.set(route, callback);
        }
    
        // Fire this function when you want to change page, for example router.change("/user/1")
        // It will also save the route change to history api.
        change(route) {
            window.history.pushState({ action: 'changeRoute' }, null, route);
            window.dispatchEvent(new Event('popstate'));
        }
    }
    
    export default new Router();
    
    



    ルーターの使用



    PS!

    You should also, add <base href="/"> in the header of your HTML file, so the front-end router, will always start the URL path from the start, and will not keep appending to the URL.




    まず、Router をインポートします.

    ES6 ネイティブ モジュール インポートを使用します.これは非常に簡単で、ほとんどのブラウザーで既にサポートされています.

    import Router from '/libraries/router.js';
    


    ファイルからルータークラスを新しいものとして直接エクスポートするか、次のようなことを行うことができます

    window.router = new Router()
    



    PS!

    My personal preference is to create the page as webcomponent or lit.js and then just swap the components when route is active.





    
    Router.on('/home', (event) => {
        // Replace and render page content here
    });
    
    
    Router.on('/post/:id', (event) => {
        // Replace and render page content here
        // You can get parameter with, event.parameters.id
    });
    


    ルート変更



    ルートを変更するには、以下のコードを使用する必要があります.これは、URL の変更もこの方法でブラウザーの履歴に保存されるためです.

    Router.change("/account")
    



    バックエンドのセットアップ



    Web 上で SPA アプリを作成する場合、発生する可能性のあるエラーに注意する必要があります.

    www.mybook.com/user/1 などの URL のページを読み込もうとすると、通常、バックエンドは 404 エラー、ページが見つかりませんを送信します.

    これは、バックエンドが /user/1 のルートを定義していないためです.ルート検索はフロントエンド側で行われる必要があります.

    それを修正するために、バックエンドの 404 ルートを index.html ファイルまたは使用しているファイルにリダイレクトします.

    そのため、ルートが見つからないバックエンド送信の代わりに、SPA アプリのメイン ファイルが送信され、ルートに関する情報があるため、SPA アプリ ルーターは正しいページをレンダリングします.


    バックエンド プロキシに使用するツール



    ローカルでデバッグするために、Node.jshttp-server を使用しています

    このコンソール コマンドは、現在のフォルダーで http-server を実行し、失敗したすべての要求をメインの index.html にリダイレクトし、JS ルーターが引き継ぎます.
    http-server -p 8080 . --proxy http://localhost:8080?
    本番環境では、バックエンド プロキシとして Caddy を使用しています.
    だからここに私がすべての 404 リクエストを Caddy の index.html に送信するコード例があります.
    try_files の部分は、失敗したルートがリダイレクトされる場所です.

    https://www.mybook.com {
        root * /srv/www/mybook
        try_files {path} /index.html    
        encode zstd gzip
        file_server
    }