1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

フロントエンドのみで図書検索システムを考える

Last updated at Posted at 2025-03-13

1.書籍検索システムの概要

1.1 画面の完成図

image.png

ユーザーの個人情報等は一切扱わず、あくまでもデータベースの中に何があるのかを検索するだけの簡易的なシステムを考えます。

システムといってますが、ただのHTMLファイル(WEBページ)です。
しかも、インターネットにアクセスすることはありません。あくまで検索だけの用途です。
特に入力欄のエスケープ処理などもしていません。

まず考えたのは起動時に、例えばエクセルファイルやcsvのような形式のファイルから、書籍のデータベースを読み込むことです。しかし、ブラウザの権限ではユーザの入力なしに、ファイルを読み込むことができません。

EXCELファイルやcsvファイルなどを読み込むことも考えられそうですが、ユーザーがひと手間加える必要が出てきてしまいます。

そこでjavascriptのスクリプト部分にデータ自体を入れておくことを考えました。
ただし、データ部分のみを更新するほうがメンテナンスしやすいと考えたので、jsonファイルとして別ファイルに格納しておきます。

書籍のデータベースをdatabase.jsonの形に落とし込み、jsonファイルとします。

別途用意したさまざまな形式のデータをjson化する方法については、また別の機会に扱いたいと思います。

1.2 同じ階層にjsonファイルを作成

index.htmlを開いたと同時にカレントディレクトリのjsonファイルを読み込みます。

index.html
<!DOCTYPE html>
<html lang="ja">
<head>
    ~省略~
</head>
<body>
    ~省略~
</body>
</script>
    <script>
        // 外部JSONファイルを読み込む
        fetch('database.json')

以下のようなjsonデータ(仮)があるものとします。

配列内にjsonデータが格納されているだけです。

jsonに設定した属性は以前書いた記事で取得した書籍情報の属性を参考にしています。

database.json
[
    {
        "登録番号": "001",
        "ISBN": "978-4-1234-5678-9",
        "タイトル": "プログラミング入門",
        "タイトルカナ": "プログラミングニュウモン",
        "著者": "山田 太郎",
        "作者": "ヤマダ タロウ",
        "作者カナ": "ヤマダ タロウ",
        "シリーズ名": "プログラミングシリーズ",
        "シリーズ名カナ": "プログラミングシリーズ",
        "発行年": "2020",
        "ページ数": "350",
        "出版社": "テクノ出版",
        "説明": "プログラミング初心者向けの入門書です。",
        "価格": "2500",
        "請求記号": "004.3"
    },
    {
        "登録番号": "002",
        "ISBN": "978-4-5678-1234-0",
        "タイトル": "データベースの基礎",
        "タイトルカナ": "データベースノキソ",
        "著者": "佐藤 花子",
        "作者": "サトウ ハナコ",
        "作者カナ": "サトウ ハナコ",
        "シリーズ名": "データベースシリーズ",
        "シリーズ名カナ": "データベースシリーズ",
        "発行年": "2018",
        "ページ数": "420",
        "出版社": "学術出版",
        "説明": "データベースの基本的な概念を解説した書籍。",
        "価格": "3000",
        "請求記号": "007.6"
    }
]

2.検索システムを作る

2.1 画面の作成

まずbodyタグ内にHTMLでチェックボックス等のウィジェットを配置します。このHTML部分がシステムの外観になっていきます。scriptタグ内がJavascript部分になり、ユーザからの入力に対する動作の部分になっています。

また、styleタグやcssファイルは外観のデザインに関するルールを適用します。HTMLだけで記述も可能ですが、いちから記述するよりもスピーディーに外観のデザインを変更することができます。

外観であるHTMLはbodyタグ内に、動作部分であるJavascriptはscriptタグ内に記述していきます。

HTMLとjavascript,cssの連携についていくつかの例を示します。

2.2 画面の表示/非表示の切り替えと非同期処理による画面待機

index.html
<head>
    <style>
      #loading {
        display: none; /* 最初は非表示 */
      }
    </style>
</head>
<body>
    <div id="loading">データを読み込み中です。お待ちください...</div>
</body>
<script>
    ~ json読み込みの処理~
    document.getElementById('loading').style.display = 'block';
</script>

要するにjsonファイルを読み込むまでの数秒の間「データを読み込み中です。お待ちください...」を表示し、読み込みが終了したら、非表示にするというものです。blockというのは横幅をいっぱいにして表示みたいな感じの意味です、つまりここでは予めnone(非表示)になっているものを表示するという意味になります。

ここで重要なのは、id('loading')をターゲットにして処理を行っているということです。
BODYタグにあるdiv要素にもid="loading"が設定されています。
このようにidを設定しておくことによってJavascript側でのコントロールが可能になります。

index.html
    <script>
        // 外部JSONファイルを読み込む
        fetch('database.json')
            .then(response => {
                // データの読み込み中に「お待ちください」を表示
                document.getElementById('loading').style.display = 'block';

                // 3秒間待つ処理を追加
                return new Promise((resolve) => {
                    setTimeout(() => {
                        resolve(response.json()); // 3秒後にデータを返す
                    }, 3000); // 3000ミリ秒(3秒)待機
                });
            })
            
            .then(database => {
                // 読み込みが完了したら「お待ちください」を非表示
                document.getElementById('loading').style.display = 'none';

ちなみに、あまりに処理が早すぎると「お待ち下さい」が見えない(くらいに早く消えてしまう)ことがあるので、3秒は待つという処理を入れてあります。非同期処理といって、読み込みは行われている間も待ち時間のカウントは行われています。非同期処理は

同期処理:読み込み処理の完了後に3秒待つを行う。
非同期処理:読み込み処理と3秒待つを並行して行う。

database.jsonを取得した後は、再びお待ち下さいを非表示にしています。

2.3 クリックイベントのキャッチ

index.html
 document.getElementById('searchButton').addEventListener('click', () => {
 }

idがsaerchButtonとなるウィジェットがユーザによってクリックさせると発生するclickイベントをキャッチし、{}内に記述した処理を行います。

このように、イベントを発生させるウィジェット(ボタンとかWEB上のページを構成する部品)の場合はEventListnerと呼ばれるメソッドでイベントの発生をキャッチし、処理内容を記述していきます。

2.4 検索処理の実行

index.html
const query = document.getElementById('searchInput').value.trim().toLowerCase();

検索のIDは'searchInput'です。検索欄に入力されたスペースを除去したり.trim()、小文字に変換したり.toLowerCase()しています。取得したオブジェクトに対し「.メソッド名」をつけることでよく使う処理を適用することができます。

※ここではtoLowerCase()を実行していますが日本語の検索の場合はアルファベットの小文字変換処理はあまり意味がないかもしれません。

index.html
            const query = document.getElementById('searchInput').value.trim().toLowerCase();

            const searchTitle = document.getElementById('searchTitle').checked;

            const results = database.filter(item => {
                let match = false;

                if (searchTitle) {
                    if (item.タイトル && item.タイトル.toLowerCase().includes(query)) match = true;
                    if (item.タイトルカナ && item.タイトルカナ.toLowerCase().includes(query)) match = true;
                    if (item.説明 && item.説明.toLowerCase().includes(query)) match = true;
                    if (item.シリーズ名 && item.シリーズ名.toLowerCase().includes(query)) match = true;
                    if (item.シリーズ名カナ && item.シリーズ名カナ.toLowerCase().includes(query)) match = true;
                    }

document.getElementById('searchTitle').checkedで、チェックボックスがチェックされているか確認します。チェックされていればタイトルで検索を行います。

index.html
            .then(database => {
                // 読み込みが完了したら「お待ちください」を非表示
                document.getElementById('loading').style.display = 'none';

database.jsonをすでに読み込んでいるので、const results = database.filter(item => {}によって、item.タイトルのようにデータにアクセスできます。ここでitem.以降にくる属性名は読み込んだjsonファイルと対応しています。

ちなみに、ここではタイトル、カナ、説明、シリーズ名からも検索を行うような仕様にしています。ここは、少しもっとブラッシュアップする必要があるかもしれません。

例えば、現状半角カナや、カタカナ・ひらがなの別の文字として認識されます。

database.json
[
    {
        "登録番号": "001",
        "ISBN": "978-4-1234-5678-9",
        "タイトル": "プログラミング入門",
        "タイトルカナ": "プログラミングニュウモン",
        "著者": "山田 太郎",
        "作者": "ヤマダ タロウ",
        "作者カナ": "ヤマダ タロウ",
        "シリーズ名": "プログラミングシリーズ",
        "シリーズ名カナ": "プログラミングシリーズ",
        "発行年": "2020",
        "ページ数": "350",
        "出版社": "テクノ出版",
        "説明": "プログラミング初心者向けの入門書です。",
        "価格": "2500",
        "請求記号": "004.3"
    },

3.Bootstrap適用前のコード

見た目は後からでも変更できるので、とりあえず動くか確認してみます。
概ね問題はなさそうです。

index.html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>ローカルデータベース検索</title>
    <style>
        body { font-family: Arial, sans-serif; padding: 20px; }
        .result, .recommendation { margin-top: 20px; }
        button { margin-top: 10px; }
        #loading { display: none; color: #ff0000; font-weight: bold; }
    </style>
</head>
<body>
    <h1>ローカルデータベース検索</h1>
    <input type="text" id="searchInput" placeholder="検索ワードを入力">
    
    <h3>検索対象:</h3>
    <label>
        <input type="checkbox" id="searchTitle" checked> タイトル / 内容等
    </label><br>
    <label>
        <input type="checkbox" id="searchAuthor"> 著者 / 作者
    </label><br>
    <label>
        <input type="checkbox" id="searchPublisher"> 出版社
    </label><br>
    
    <h3>発行年で絞り込み:</h3>
    <label>
        <input type="checkbox" id="searchYear"> 発行年で検索
    </label><br>
    
    <label for="yearStart">開始年:</label>
    <input type="number" id="yearStart" placeholder="開始年" disabled><br>
    <label for="yearEnd">終了年:</label>
    <input type="number" id="yearEnd" placeholder="終了年" disabled><br>
    
    <button id="searchButton">検索</button>

    <div id="loading">データを読み込み中です。お待ちください...</div>  <!-- ここに「お待ちください」を表示 -->

    <div class="result">
        <h2>検索結果:</h2>
        <div id="searchResults"></div>
    </div>

    <script>
        // 外部JSONファイルを読み込む
        fetch('database.json')
            .then(response => {
                // データの読み込み中に「お待ちください」を表示
                document.getElementById('loading').style.display = 'block';

                // 3秒間待つ処理を追加
                return new Promise((resolve) => {
                    setTimeout(() => {
                        resolve(response.json()); // 3秒後にデータを返す
                    }, 3000); // 3000ミリ秒(3秒)待機
                });
            })
            
            .then(database => {
                // 読み込みが完了したら「お待ちください」を非表示
                document.getElementById('loading').style.display = 'none';

                // 発行年チェックボックスの変更時に年の入力フィールドを有効化
                document.getElementById('searchYear').addEventListener('change', () => {
                    const yearStart = document.getElementById('yearStart');
                    const yearEnd = document.getElementById('yearEnd');
                    const isYearChecked = document.getElementById('searchYear').checked;

                    yearStart.disabled = !isYearChecked;
                    yearEnd.disabled = !isYearChecked;
                });

                // 検索ボタンの動作
                document.getElementById('searchButton').addEventListener('click', () => {
                    const query = document.getElementById('searchInput').value.trim().toLowerCase();

                    // 検索対象の選択された項目を取得
                    const searchTitle = document.getElementById('searchTitle').checked;
                    const searchAuthor = document.getElementById('searchAuthor').checked;
                    const searchPublisher = document.getElementById('searchPublisher').checked;
                    const searchYear = document.getElementById('searchYear').checked;

                    // 検索処理
                    const results = database.filter(item => {
                        let match = false;

                        // タイトル / 内容等 がチェックされていたら、それに対応する検索を実施
                        if (searchTitle) {
                            if (item.タイトル && item.タイトル.toLowerCase().includes(query)) match = true;
                            if (item.タイトルカナ && item.タイトルカナ.toLowerCase().includes(query)) match = true;
                            if (item.説明 && item.説明.toLowerCase().includes(query)) match = true;
                            if (item.シリーズ名 && item.シリーズ名.toLowerCase().includes(query)) match = true;
                            if (item.シリーズ名カナ && item.シリーズ名カナ.toLowerCase().includes(query)) match = true;
                        }

                        // 著者 / 作者がチェックされていたら、それに対応する検索を実施
                        if (searchAuthor) {
                            if (item.著者 && item.著者.toLowerCase().includes(query)) match = true;
                            if (item.作者 && item.作者.toLowerCase().includes(query)) match = true;
                            if (item.作者カナ && item.作者カナ.toLowerCase().includes(query)) match = true;
                        }

                        // 出版社がチェックされていたら、それに対応する検索を実施
                        if (searchPublisher && item.出版社 && item.出版社.toLowerCase().includes(query)) match = true;

                        // 発行年の条件でフィルタリング
                        if (searchYear) {
                            const year = parseInt(item.発行年, 10);  // 発行年を整数として取得
                            const yearStart = document.getElementById('yearStart').value;
                            const yearEnd = document.getElementById('yearEnd').value;

                            if (!isNaN(year)) {
                                if (yearStart && year < parseInt(yearStart)) match = false;  // 開始年より前
                                if (yearEnd && year > parseInt(yearEnd)) match = false;    // 終了年より後
                            }
                        }

                        return match;
                    });
                    
                    displayResults(results);
                });

                // 検索結果を表示する
                function displayResults(results) {
                    const resultsDiv = document.getElementById('searchResults');
                    const totalCount = results.length;

                    if (totalCount > 0) {
                        resultsDiv.innerHTML = `<div><strong>検索結果: ${totalCount} 件見つかりました。</strong></div><hr>` +
                            results.map((item, index) => {
                                return `
                                    <hr>
                                    <div>
                                        ${index + 1}. タイトル: ${item.タイトル || '不明'}<br>
                                        タイトルカナ: ${item.タイトルカナ || '不明'}<br>
                                        シリーズ名: ${item.シリーズ名 || '不明'}<br>
                                        シリーズ名カナ: ${item.シリーズ名カナ || '不明'}<br>
                                        説明: ${item.説明 || '不明'}<br>
                                        著者: ${item.著者 || '不明'}<br>
                                        作者: ${item.作者 || '不明'}<br>
                                        作者カナ: ${item.作者カナ || '不明'}<br>
                                        出版社:${item.出版社 || '不明'}<br>
                                        発行年: ${item.発行年 || '不明'}<br>
                                    </div>
                                    <hr>
                                `;
                            }).join('');
                    } else {
                        resultsDiv.innerHTML = "見つかりませんでした。";
                    }
                }


            })
            .catch(error => {
                console.error('JSONの読み込みエラー:', error);
                document.getElementById('loading').innerHTML = "データの読み込みに失敗しました。";
            });
    </script>
</body>
</html>

4.Bootstrapを適用してそれっぽく仕上げてみた

index.html
<head>
    <!-- Bootstrap CSS の読み込み -->
    <link href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.0/css/bootstrap.min.css" rel="stylesheet">
</head>

Bootstrapを適用します。
ローカルで動かす関係上、Bootstrapはダウンロードしたものを使っています。

index.html
<body>
    <div class="container mt-5">
        <h1 class="text-center">図書検索システム</h1>

div要素などにclassを適用していきます。

以下、全体のコード。

index.html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Bootstrap テスト</title>
    <!-- Bootstrap CSS の読み込み -->
    <link href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.0/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>

    <div class="container mt-5">
        <h1 class="text-center">図書検索システム</h1>

        <hr>
        <div class="mb-3">
            <label for="exampleFormControlInput1" class="form-label">図書の検索</label>
            <input type="text" class="form-control" id="searchInput" placeholder="検索ワードを入力">
        </div>

        <div class="row">
            <div class="row">
                <h3>検索対象</h3>
            </div>
            <div class="col">
                <label>
                    <input type="checkbox" id="searchTitle" checked> タイトル / 内容等
                </label><br>
            </div>
            <div class="col">
                <label>
                    <input type="checkbox" id="searchAuthor"> 著者 / 作者
                </label><br>
            </div>
            <div class="col">
                <label>
                    <input type="checkbox" id="searchPublisher"> 出版社
                </label><br>
            </div>
        </div>
        
        <hr>

        <div class="mb-3">
            <div class="mb-3">
                <h3>発行年で絞り込み</h3>
            </div>
            <div class="mb-3">
                <label>
                    <input type="checkbox" id="searchYear"> 発行年で検索
                </label><br>
            </div>
            <div class="mb-3">
                <div class="row">
                    <div class="col">
                        <label for="yearStart">開始:</label>
                        <input type="number" id="yearStart" placeholder="西暦で入力" disabled>
                    </div>
                    <div class="col">
                        <label for="yearEnd">終了:</label>
                        <input type="number" id="yearEnd" placeholder="西暦で入力" disabled>
                    </div>
                </div>
            </div>
        </div>

        <div class="row">
            <button type="button" class="btn btn-primary btn-lg" id="searchButton">検索</button>
        </div>
    
        <div id="loading" style="display: none;">データを読み込み中です。お待ちください...</div>  <!-- 読み込み中の表示を非表示に変更 -->

    </div>
    <div class="container mt-5">
        <h2>検索結果:</h2>
        <div id="searchResults"></div>
    </div>

    <!-- Bootstrap JavaScript の読み込み -->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.0/js/bootstrap.bundle.min.js"></script>
    <script>
        // 外部JSONファイルを読み込む
        fetch('database.json')
            .then(response => response.json())
            .then(database => {
                document.getElementById('loading').style.display = 'block';

                return new Promise((resolve) => {
                    setTimeout(() => {
                        document.getElementById('loading').style.display = 'none';
                        resolve(database);
                    }, 3000);
                });
            })
            
            .then(database => {
                // 発行年チェックボックスの変更時に年の入力フィールドを有効化
                document.getElementById('searchYear').addEventListener('change', () => {
                    const yearStart = document.getElementById('yearStart');
                    const yearEnd = document.getElementById('yearEnd');
                    const isYearChecked = document.getElementById('searchYear').checked;

                    yearStart.disabled = !isYearChecked;
                    yearEnd.disabled = !isYearChecked;
                });

                // 検索ボタンの動作
                // 検索ボタンの動作
                document.getElementById('searchButton').addEventListener('click', () => {
                    const query = document.getElementById('searchInput').value.trim().toLowerCase();

                    // 検索対象の選択された項目を取得
                    const searchTitle = document.getElementById('searchTitle').checked;
                    const searchAuthor = document.getElementById('searchAuthor').checked;
                    const searchPublisher = document.getElementById('searchPublisher').checked;
                    const searchYear = document.getElementById('searchYear').checked;

                    // 検索処理
                    const results = database.filter(item => {
                        let match = false;

                        // タイトル / 内容等 がチェックされていたら、それに対応する検索を実施
                        if (searchTitle) {
                            if (item.タイトル && item.タイトル.toLowerCase().includes(query)) match = true;
                            if (item.タイトルカナ && item.タイトルカナ.toLowerCase().includes(query)) match = true;
                            if (item.説明 && item.説明.toLowerCase().includes(query)) match = true;
                            if (item.シリーズ名 && item.シリーズ名.toLowerCase().includes(query)) match = true;
                            if (item.シリーズ名カナ && item.シリーズ名カナ.toLowerCase().includes(query)) match = true;
                        }

                        // 著者 / 作者がチェックされていたら、それに対応する検索を実施
                        if (searchAuthor) {
                            if (item.著者 && item.著者.toLowerCase().includes(query)) match = true;
                            if (item.作者 && item.作者.toLowerCase().includes(query)) match = true;
                            if (item.作者カナ && item.作者カナ.toLowerCase().includes(query)) match = true;
                        }

                        // 出版社がチェックされていたら、それに対応する検索を実施
                        if (searchPublisher && item.出版社 && item.出版社.toLowerCase().includes(query)) match = true;

                        // 発行年の条件でフィルタリング
                        if (searchYear) {
                            const year = parseInt(item.発行年, 10);  // 発行年を整数として取得
                            const yearStart = document.getElementById('yearStart').value;
                            const yearEnd = document.getElementById('yearEnd').value;

                            if (!isNaN(year)) {
                                if (yearStart && year < parseInt(yearStart)) match = false;  // 開始年より前
                                if (yearEnd && year > parseInt(yearEnd)) match = false;    // 終了年より後
                            }
                        }

                        return match;
                    });
                    
                    displayResults(results);
                });

                function displayResults(results) {
                    const resultsDiv = document.getElementById('searchResults');
                    const totalCount = results.length;

                    if (totalCount > 0) {
                        resultsDiv.innerHTML = `<div><strong>検索結果: ${totalCount} 件見つかりました。</strong></div><hr>` +
                            results.map((item, index) => {
                                return `
                                    <div class="card">
                                        ${index + 1}. タイトル: ${item.タイトル || '不明'}<br>
                                        説明: ${item.説明 || '不明'}<br>
                                        著者: ${item.著者 || '不明'}<br>
                                        出版社:${item.出版社 || '不明'}<br>
                                        発行年: ${item.発行年 || '不明'}<br>
                                        <div>
                                            <img class="img-thumbnail" src="./img/${item.ISBN}.jpg" alt="書影データなし">
                                        </div>
                                    </div>
                                    <hr>
                                `;
                            }).join('');
                    } else {
                        resultsDiv.innerHTML = "見つかりませんでした。";
                    }
                }
            })
            .catch(error => {
                console.error('JSONの読み込みエラー:', error);
                document.getElementById('loading').innerHTML = "データの読み込みに失敗しました。";
            });
    </script>
</body>
</html>

5.まとめ

多分、データ量がちょっと膨大になってくるとうまく動かないかもしれません。

何かの足しになれば幸いです。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?