0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Docker 環境と Vanilla JS (JavaScript) で最小フロントエンドを実装する:ASP.NET Core + MySQL

Last updated at Posted at 2023-08-25

Docker 環境と Vanilla JS (JavaScript) で最小フロントエンドを実装する:ASP.NET Core + MySQL

こんにちは、@studio_meowtoon です。今回は、WSL の Ubuntu 22.04 で JavaScript Vanilla JS Webアプリケーションを作成し、最小限のフロントエンドを実装する方法を紹介します。
vanilla-js_and_aspnet-core_and_mysql_on_docker.png

目的

Windows 11 の Linux でクラウド開発します。

こちらから記事の一覧がご覧いただけます。

実現すること

ローカル環境の Ubuntu の Docker 環境で、Dockerfile からビルドした JavaScript Vanilla JS Web アプリのカスタムコンテナと、REST API サービスコンテナ、MySQL データベースコンテナを起動します。

HTML ファイル形式のアプリをコンテナとして起動

実行環境

要素 概要
web-browser Web ブラウザ
Ubuntu OS
Docker コンテナ実行環境

Web アプリ コンテナ

要素 概要
app-todo-vanillajs Web アプリ カスタムコンテナ
nginx Web サーバー
index.html HTML アプリケーション

REST API サービス コンテナ

要素 概要
api-todo-aspnet-core REST API サービス カスタムコンテナ
dotnet .NET 実行環境
kestrel Web サーバー
WebApp.dll .NET アプリケーション

データベース コンテナ

要素 概要
mysql-todo データベースコンテナ
mysql DB サーバー
db_todo データベース

技術トピック

Vanilla JS とは?

こちらを展開してご覧いただけます。

Vanilla JS (バニラジェーエス)

Vanilla JS は、純粋な形式の JavaScript のことを指します。つまり、ライブラリやフレームワークを使用せずに、ブラウザ上で提供される標準の JavaScript を指します。

キーワード 内容
軽量 ライブラリやフレームワークに比べて軽量であるため、ページの読み込みや実行速度が速くなります。
直感的 基本的な JavaScript の文法や機能をそのまま使うため、JavaScript 自体の概念や原則を学ぶことに集中できます。
柔軟性 ライブラリやフレームワークの制約を受けずに、自分のプロジェクトのニーズに合わせてコードを書けます。
学習効果 Vanilla JS を学ぶことで、JavaScript の基本的なコンセプトやプログラミングスキルを磨くことができます。
カスタマイズ性 プロジェクトごとに最適なアプローチを選べるため、柔軟なカスタマイズが可能です。
依存性の低減 ライブラリやフレームワークに頼らずに開発するため、外部依存を最小限に抑えることができます。

Vanilla JS は、基本的なウェブ開発のスキルを習得し、カスタムなソリューションを構築する際に非常に役立つアプローチです。

開発環境

  • Windows 11 Home 22H2 を使用しています。

WSL の Ubuntu を操作していきますので macOS の方も参考にして頂けます。

WSL (Microsoft Store アプリ版) ※ こちらの関連記事からインストール方法をご確認いただけます

> wsl --version
WSL バージョン: 1.0.3.0
カーネル バージョン: 5.15.79.1
WSLg バージョン: 1.0.47

Ubuntu ※ こちらの関連記事からインストール方法をご確認いただけます

$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 22.04.1 LTS
Release:        22.04

npm ※ こちらの関連記事からインストール方法をご確認いただけます

$ node -v
v19.8.1
$ npm -v
9.5.1

Docker ※ こちらの関連記事からインストール方法をご確認いただけます

$ docker --version
Docker version 23.0.1, build a5ee5b1

この記事では基本的に Ubuntu のターミナルで操作を行います。Vim を使用してコピペする方法を初めて学ぶ人のために、以下の記事で手順を紹介しています。ぜひ挑戦してみてください。

作成するアプリの外観

image.png

REST API サービス コンテナと、データベース コンテナの起動

こちらの記事で、ToDo アプリ用の REST API サービスと、RDBMS データベースを作成し、Docker コンテナとして起動する手順をご確認いただけます。

REST API サービス、データベース コンテナが起動していることを確認します。

$ docker ps
CONTAINER ID   IMAGE                  COMMAND                   CREATED          STATUS          PORTS                                                  NAMES
7e4f2137658d   api-todo-aspnet-core   "dotnet WebApp.dll"       48 seconds ago   Up 47 seconds   0.0.0.0:5000->80/tcp, :::5000->80/tcp                  api-local
63a3acec271c   mysql-base             "docker-entrypoint.s…"   43 hours ago     Up 6 minutes    0.0.0.0:3306->3306/tcp, :::3306->3306/tcp, 33060/tcp   mysql-todo

コンテナ間通信するために net-todo という Docker ネットワークを予め作成しています。ご注意ください。

REST クライアント を実装する手順

プロジェクトの作成

プロジェクトフォルダを作成します。
※ ~/tmp/restclt-vanillajs をプロジェクトフォルダとします。

$ mkdir -p ~/tmp/restclt-vanillajs
$ cd ~/tmp/restclt-vanillajs

JS ファイルの作成

main.js ファイルを作成します。

$ mkdir -p src
$ vim src/main.js

ファイルの内容

コードの全体を表示する
src/main.js
import './styles.css'

const _api_url = process.env.API_URL; // API URL
const _div_todos = document.getElementById("div-todo-list"); // ToDo テーブル
const _button_add = document.getElementById("button-add"); // 新規追加ボタン
const _input_add = document.getElementById("input-add"); // 新規追加テキスト
const _input_edit = document.createElement("input"); // 更新テキスト
const _todos = []; // データ保持配列

// データの取得と表示
const fetch_and_render = async () => {
    try {
        const response = await fetch(`${_api_url}/todos`);
        if (!response.ok) { alert(`Fetch failed with status: ${response.status}`); }
        const data = await response.json();
        const filtered = data.filter(elem => elem.completed_date === null) // 未完了のアイテムのみフィルタリング
        const updated = filtered.map(elem => ({ // ToDo データに編集関連の情報を追加して新しい配列を作成
            ...elem, // 全ての要素を引継ぎ
            editing: false // 編集フラグ OFF を追加
        }));
        set_todos(updated); // 新しい配列をセットして ToDo データを更新
    } catch (error) { alert("Error fetching todos: ", error); }
};

// データを展開する関数
const set_todos = (todos) => {
    _div_todos.innerHTML = "";
    _todos.length = 0;
    _todos.push(...todos);
    _todos.forEach(value => {
        initialize_component(value);
    });
};

// 新規追加ボタンのクリックイベントハンドラ
const handle_button_add_click = async () => {
    try {
        const content = _input_add.value.trim();
        if (content === "") { return };
        if (confirm("アイテムを新規追加しますか?")) {
            const response = await fetch(`${_api_url}/todos`, {
                method: "POST",
                headers: { "Content-Type": "application/json" },
                body: JSON.stringify({ content: content })
            });
            if (!response.ok) { alert(`Fetch failed with status: ${response.status}`); }
            fetch_and_render();
            _input_add.value = "";
        }
    } catch (error) { alert("Error adding todo: ", error); }
};

// 完了ボタンのクリックイベントハンドラ
const handle_button_complete_click = (todo) => async () => {
    try {
        if (confirm("このアイテムを完了にしますか?")) {
            const response = await fetch(`${_api_url}/todos/${todo.id}`, {
                method: "PUT",
                headers: { "Content-Type": "application/json" },
                body: JSON.stringify({
                    content: todo.content,
                    completed_date: new Date().toISOString()
                })
            });
            if (!response.ok) { alert(`Update failed with status: ${response.status}`); }
            fetch_and_render();
        }
    } catch (error) { alert("Error updating todo: ", error); }
};

// テーブル行テキストのクリックイベントハンドラ
const handle_row_content_click = (todo) => () => {
    try {
        // 要素がアクティブなら return
        if (document.activeElement === _input_edit) { return; }
        // 編集中フラグを更新
        const updated = _todos.map(elem => ({
            ...elem,
            editing: elem.id === todo.id
        }));
        _todos.length = 0;
        _todos.push(...updated);
        _todos.forEach(value => {
            // テーブル行の初期化
            const div_content = document.getElementById(`div-content-${value.id}`);
            div_content.innerHTML = value.content;
            const button_update = document.getElementById(`button-update-${value.id}`);
            button_update.setAttribute("disabled", "true");
            // 編集中フラグが ON なら input 要素に置き換え
            if (value.editing) {
                button_update.removeAttribute("disabled"); // 更新ボタン有効
                _input_edit.value = value.content;
                _input_edit.className = "input-edit";
                div_content.innerHTML = "";
                div_content.appendChild(_input_edit);
            }
        });
    } catch (error) { alert("Error clicking todo: ", error); }
};

// 更新ボタンのクリックイベントハンドラ
const handle_button_update_click = (todo) => async () => {
    try {
        const content = _input_edit.value.trim();
        if (content === "") { return };
        if (confirm("このアイテムを更新しますか?")) {
            const response = await fetch(`${_api_url}/todos/${todo.id}`, {
                method: "PUT",
                headers: { "Content-Type": "application/json" },
                body: JSON.stringify({ content: content })
            });
            if (!response.ok) { alert(`Fetch failed with status: ${response.status}`); }
            fetch_and_render();

        }
    } catch (error) { alert("Error updating todo: ", error); }
};

// 削除ボタンのクリックイベントハンドラ
const handle_button_delete_click = (todo) => async () => {
    try {
        if (confirm("このアイテムを削除しますか?")) {
            const response = await fetch(`${_api_url}/todos/${todo.id}`, {
                method: "DELETE",
            });
            if (!response.ok) { alert(`Delete failed with status: ${response.status}`); }
            fetch_and_render();
        }
    } catch (error) { alert("Error deleting todo: ", error); }
};

// テーブルの構築
const initialize_component = (todo) => {
    // 列作成
    const div_row = document.createElement("div");
    div_row.className = "div-row";
    // ボタングループ
    const div_buttons = document.createElement("div");
    div_buttons.className = "div-buttons";
    // 完了ボタン
    const button_complete = document.createElement("button");
    button_complete.textContent = "完了";
    button_complete.className = "button-complete";
    button_complete.addEventListener("click", handle_button_complete_click(todo));
    // 更新ボタン
    const button_update = document.createElement("button");
    button_update.textContent = "更新";
    button_update.className = "button-update";
    button_update.id = `button-update-${todo.id}`;
    button_update.setAttribute("disabled", "true");
    button_update.addEventListener("click", handle_button_update_click(todo));
    // 削除ボタン
    const button_delete = document.createElement("button");
    button_delete.textContent = "削除";
    button_delete.className = "button-delete";
    button_delete.addEventListener("click", handle_button_delete_click(todo));
    // コンテンツ
    const div_content = document.createElement("div");
    div_content.className = "div-content";
    div_content.id = `div-content-${todo.id}`;
    div_content.textContent = `${todo.content}`;
    div_content.addEventListener("click", handle_row_content_click(todo));
    // テーブル要素構築
    div_buttons.appendChild(button_complete);
    div_buttons.appendChild(button_update);
    div_buttons.appendChild(button_delete);
    div_row.appendChild(div_content);
    div_row.appendChild(div_buttons);
    _div_todos.appendChild(div_row);
};

// アイテム新規追加イベント
_input_add.addEventListener("click", fetch_and_render);
_button_add.addEventListener("click", handle_button_add_click);

// 初期表示
fetch_and_render();

以下、ポイントを説明します。

src/main.js ※抜粋
// データの取得と表示
const fetch_and_render = async () => {
    try {
        const response = await fetch(`${_api_url}/todos`);
        if (!response.ok) { alert(`Fetch failed with status: ${response.status}`); }
        const data = await response.json();
        const filtered = data.filter(elem => elem.completed_date === null) // 未完了のアイテムのみフィルタリング
        const updated = filtered.map(elem => ({ // ToDo データに編集関連の情報を追加して新しい配列を作成
            ...elem, // 全ての要素を引継ぎ
            editing: false // 編集フラグ OFF を追加
        }));
        set_todos(updated); // 新しい配列をセットして ToDo データを更新
    } catch (error) { alert("Error fetching todos: ", error); }
};

// 新規追加ボタンのクリックイベントハンドラ
const handle_button_add_click = async () => {
    try {
        const content = _input_add.value.trim();
        if (content === "") { return };
        if (confirm("アイテムを新規追加しますか?")) {
            const response = await fetch(`${_api_url}/todos`, {
                method: "POST",
                headers: { "Content-Type": "application/json" },
                body: JSON.stringify({ content: content })
            });
            if (!response.ok) { alert(`Fetch failed with status: ${response.status}`); }
            fetch_and_render();
            _input_add.value = "";
        }
    } catch (error) { alert("Error adding todo: ", error); }
};

// 完了ボタンのクリックイベントハンドラ
const handle_button_complete_click = (todo) => async () => {
    try {
        if (confirm("このアイテムを完了にしますか?")) {
            const response = await fetch(`${_api_url}/todos/${todo.id}`, {
                method: "PUT",
                headers: { "Content-Type": "application/json" },
                body: JSON.stringify({
                    content: todo.content,
                    completed_date: new Date().toISOString()
                })
            });
            if (!response.ok) { alert(`Update failed with status: ${response.status}`); }
            fetch_and_render();
        }
    } catch (error) { alert("Error updating todo: ", error); }
};

// テーブル行テキストのクリックイベントハンドラ
const handle_row_content_click = (todo) => () => {
    try {
        // 要素がアクティブなら return
        if (document.activeElement === _input_edit) { return; }
        // 編集中フラグを更新
        const updated = _todos.map(elem => ({
            ...elem,
            editing: elem.id === todo.id
        }));
        _todos.length = 0;
        _todos.push(...updated);
        _todos.forEach(value => {
            // テーブル行の初期化
            const div_content = document.getElementById(`div-content-${value.id}`);
            div_content.innerHTML = value.content;
            const button_update = document.getElementById(`button-update-${value.id}`);
            button_update.setAttribute("disabled", "true");
            // 編集中フラグが ON なら input 要素に置き換え
            if (value.editing) {
                button_update.removeAttribute("disabled"); // 更新ボタン有効
                _input_edit.value = value.content;
                _input_edit.className = "input-edit";
                div_content.innerHTML = "";
                div_content.appendChild(_input_edit);
            }
        });
    } catch (error) { alert("Error clicking todo: ", error); }
};

// 更新ボタンのクリックイベントハンドラ
const handle_button_update_click = (todo) => async () => {
    try {
        const content = _input_edit.value.trim();
        if (content === "") { return };
        if (confirm("このアイテムを更新しますか?")) {
            const response = await fetch(`${_api_url}/todos/${todo.id}`, {
                method: "PUT",
                headers: { "Content-Type": "application/json" },
                body: JSON.stringify({ content: content })
            });
            if (!response.ok) { alert(`Fetch failed with status: ${response.status}`); }
            fetch_and_render();

        }
    } catch (error) { alert("Error updating todo: ", error); }
};

// 削除ボタンのクリックイベントハンドラ
const handle_button_delete_click = (todo) => async () => {
    try {
        if (confirm("このアイテムを削除しますか?")) {
            const response = await fetch(`${_api_url}/todos/${todo.id}`, {
                method: "DELETE",
            });
            if (!response.ok) { alert(`Delete failed with status: ${response.status}`); }
            fetch_and_render();
        }
    } catch (error) { alert("Error deleting todo: ", error); }
};
説明を開きます。
要素 説明
アイテム追加イベント _input_add 要素がクリックされたときに、新しいアイテムを追加するためのイベントハンドラを登録しています。_button_add 要素がクリックされたときに、handle_button_add_click() 関数が呼び出されます。
データ表示 fetch_and_render() 関数は、サーバーからデータを取得して未完了の ToDo アイテムを表示するための非同期関数です。サーバーからのデータをフィルタリングし、それぞれのアイテムに対して initialize_component() 関数を呼び出して初期化します。
アイテム追加ボタンのクリック _button_add 要素がクリックされたときに実行される関数です。入力内容を取得し、サーバーに新しいアイテムを追加するための非同期処理を行います。
完了ボタンのクリック 各アイテムの完了ボタンがクリックされたときに実行される関数です。サーバーにアイテムの完了情報を送信してアイテムのステータスを更新します。
アイテムテキスト編集 ToDo アイテムのテキストをクリックすると、その内容が編集可能な input 要素に置き換えられます。
アイテム更新と削除 更新ボタンをクリックすると、アイテムの内容がサーバーに送信されて更新されます。削除ボタンをクリックすると、アイテムがサーバーから削除されます。

HTML ファイルの作成

index.html ファイルを作成します。

$ vim src/index.html

ファイルの内容

src/index.html
<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>ToDo アプリ</title>
    </head>
    <body>
        <div class="div-container">
            <h1>ToDo リスト</h1>
            <div class="div-add">
                <input type="text" id="input-add" placeholder="新規アイテムを追加">
                <button id="button-add">新規追加</button>
            </div>
            <div class="div-table" id="div-todo-list"></div>
        </div>
    </body>
</html>

CSS ファイルの作成

styles.css ファイルを作成します。

$ vim src/styles.css

ファイルの内容

コードの全体を表示する
src/styles.css
body {
    font-family: Arial, sans-serif;
    margin: 0;
    padding: 0;
}

h1 {
    text-align: center;
    margin: 20px 0;
}

/* コンテナ */
.div-container {
    max-width: 800px;
    margin: 0 auto;
}

/* テーブル */
.div-table {
    display: table;
    width: 100%;
    max-width: 800px;
    margin: 0 auto;
    border-collapse: collapse;
    border: 1px solid #ddd;
}

/* テーブルの行 */
.div-row {
    display: table-row;
}

/* 行のテキストセル */
.div-content {
    display: table-cell;
    padding: 10px;
    border-top: 1px solid #ddd;
    flex: 1;
    text-align: left;
    width: 68%;
}

/* ボタングループセル */
.div-buttons {
    display: table-cell;
    padding: 10px;
    border-top: 1px solid #ddd;
    text-align: right;
}

/* 完了ボタン */
.button-complete {
    margin-left: 10px;
    padding: 8px 12px;
    background-color: royalblue;
    color: #fff;
    border: none;
    border-radius: 4px;
    cursor: pointer;
}
@media (max-width: 768px) {
    .button-complete {
        margin-left: 2px;
        padding: 5px 2px;
    }
}

/* 更新ボタン */
.button-update {
    margin-left: 10px;
    padding: 8px 12px;
    background-color: mediumseagreen;
    color: #fff;
    border: none;
    border-radius: 4px;
    cursor: pointer;
}
@media (max-width: 768px) {
    .button-update {
        margin-left: 2px;
        padding: 5px 2px;
    }
}

/* 更新ボタン:無効 */
.button-update:disabled {
    background-color: gray;
    cursor: not-allowed;
}

/* 更新テキスト */
.input-edit {
    padding: 8px;
    background-color: lightyellow;
    font-size: 16px;
    border: 1px solid #ccc;
    border-radius: 4px;
    width: 100%;
}

/* 削除ボタン */
.button-delete {
    margin-left: 10px;
    padding: 8px 12px;
    background-color: indianred;
    color: #fff;
    border: none;
    border-radius: 4px;
    cursor: pointer;
}
@media (max-width: 768px) {
    .button-delete {
        margin-left: 2px;
        padding: 5px 2px;
    }
}

/* 新規追加エリア */
.div-add {
    margin-bottom: 20px;
    display: flex;
    align-items: center;
    width: 100%;
}

/* 新規追加テキスト */
.div-add input {
    flex: 1;
    padding: 8px;
    font-size: 16px;
    border: 1px solid #ccc;
    border-radius: 4px;
}
.div-add input:focus { /* フォーカス */
    background-color: lightyellow; 
}

/* 新規追加ボタン */
.div-add button {
    margin-left: 10px;
    padding: 8px 12px;
    background-color: royalblue;
    color: #fff;
    border: none;
    border-radius: 4px;
    cursor: pointer;
}

以下、ポイントを説明します。

src/styles.css ※抜粋
/* テーブル */
.div-table {
    display: table;
    /* 省略 */
}

/* テーブルの行 */
.div-row {
    display: table-row;
}

/* 行のテキストセル */
.div-content {
    display: table-cell;
    /* 省略 */
}

/* ボタングループセル */
.div-buttons {
    display: table-cell;
   /* 省略 */
}
要素 内容
display: table 要素をテーブルとして表示するために使用されます。
display: table-row テーブル内の行を表すために使用されます。
display: table-cell テーブル内のセル (セル内のデータやコンテンツ) を表すために使用されます。

ビルドと実行

Webpack の設定ファイルを作成します。

$ vim webpack.config.js

ファイルの内容

webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const webpack = require('webpack');

module.exports = {
  mode: 'production',
  entry: './src/main.js',
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'build')
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: 'babel-loader'
      },
      {
        test: /\.css$/,
        use: ['style-loader', 'css-loader']
      }
    ]
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: './src/index.html'
    }),
    new webpack.DefinePlugin({
      'process.env.API_URL': JSON.stringify(process.env.API_URL)
    })
  ]
};
説明を開きます。
要素 内容
mode ビルドモードを指定します。
entry アプリケーションのエントリーポイントとなる JavaScript ファイル (ここでは main.js) を指定します。このファイルを起点にして依存関係を解決してバンドルします。
output バンドルされたファイル (ここでは bundle.js) の出力先を指定します。filename で出力ファイル名、path で出力先ディレクトリの絶対パスを指定します。
module.rules ファイルのローダー(変換ツール)を定義します。test でファイルの正規表現を指定し、ローダーを適用するファイルを絞り込みます。use で使用するローダーを指定します。ここでは、JavaScript ファイルには babel-loader を適用し、CSS ファイルには style-loader と css-loader を順番に適用します。
plugins プラグインを設定します。ここでは HtmlWebpackPlugin と DefinePlugin が設定されています。HtmlWebpackPlugin は HTML ファイルを生成するプラグインで、template オプションで指定した HTML ファイルを元に、バンドルされた JavaScript ファイルを自動的に挿入します。DefinePlugin は環境変数をバンドル内で利用可能にします。ここでは process.env.API_URL を定義しています。

プロジェクトの初期化を行います。

$ npm init -y

package.json を修正します。

$ vim package.json

ファイルの内容

package.json
{
  "name": "restclt-vanillajs",
  "version": "1.0.0",
  "description": "",
  "main": "webpack.config.js",
  "scripts": {
    "build": "webpack --mode production"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}
説明を開きます。
要素 内容
name プロジェクトの名前。一意の識別子として使用されます。
version プロジェクトのバージョン。通常はメジャーバージョン.マイナーバージョン.パッチバージョンの形式です。
description プロジェクトの簡単な説明。ここでは空です。
main プロジェクトのエントリーポイントとなるファイル。ここでは webpack.config.js と指定されています。
scripts タスクやコマンドを定義するスクリプトセクションです。ここでは build スクリプトが定義されています。npm run build コマンドを実行すると、Webpack がプロジェクトをビルドします。
keywords プロジェクトに関連するキーワード。ここでは空です。
author プロジェクトの作者。ここでは空です。
license プロジェクトのライセンスを示す。ここでは ISC ライセンスと指定されています。

ライブラリをインストールします。

$ npm install \
    webpack webpack-cli html-webpack-plugin \
    css-loader style-loader babel-loader \
    @babel/core @babel/preset-env \
    --save-dev
説明を開きます。
要素 内容
webpack Webpack のコアパッケージです。
webpack-cli Webpack のコマンドラインインターフェースです。
html-webpack-plugin バンドルされたスクリプトを提供するための HTML ファイルを生成します。
css-loader CSS のインポートを処理します。
style-loader CSS スタイルを HTML に注入します。
babel-loader Babel を使用して JavaScript をトランスパイルします。
@babel/core Babel のコア機能です。
@babel/preset-env Babel を設定してモダンな JavaScript 構文を変換するためのプリセットです。

環境変数を作成します。

$ export API_URL=http://localhost:5000

API サービス コンテナの URL を設定してます。この環境変数が一時的なものであることに注意してください。

ビルドします。

$ npm run build

アプリを起動します。
※ アプリを停止するときは ctrl + C を押します。

$ python3 \
    -m http.server 8000 \
    --directory ./build
説明を開きます。
要素 内容
python3 Python のインタープリターを起動するコマンドです。
-m http.server Python の組み込みモジュールである http.server モジュールを指定して起動します。このモジュールを使用することで、簡単な HTTP サーバーをローカルに立ち上げることができます。
8000 サーバーが使用するポート番号です。ここではポート番号 8000 を指定していますが、必要に応じて変更することができます。
--directory ./build オプションとして --directory を使用し、提供したディレクトリ (./build ディレクトリ) をルートとするよう指定します。これにより、指定したディレクトリ内のファイルが提供されます。

Web ブラウザで確認します。

$ wslview http://localhost:8000

ここまでの手順で、Ubuntu でアプリを Web サーバー上で起動することができました。

コンテナイメージの作成

Nginx の設定ファイルを作成します。

$ vim nginx.conf

ファイルの内容

nginx.conf
user nginx;
worker_processes 3; # ワーカープロセス数を 3 に設定:調整してください。
events {
    worker_connections 256; # 同時接続数の最大値を設定:調整してください。
}
http {
    include /etc/nginx/mime.types;
    default_type application/octet-stream;
    server {
        listen 80;
        server_name localhost;
        location / {
            root /usr/share/nginx/html;  # 静的ファイルのルートディレクトリ
            index index.html;  # デフォルトのインデックスファイル
        }
    }
}

Dockerfile を作成します。

$ vim Dockerfile

ファイルの内容

Dockerfile
# build the app.
FROM node:lts-bookworm-slim as build-env

# set the working dir.
WORKDIR /app

# copy json files to the working dir.
COPY package.json package-lock.json /app/
COPY webpack.config.js /app/

# copy the source files to the working dir.
COPY src /app/src

# set the environment.
ARG API_URL
ENV API_URL=$API_URL

# run the build command to build the app.
RUN npm install && \
    npm run build

# set up the production container.
FROM nginx:bookworm

# copy nginx.conf.
COPY nginx.conf /etc/nginx/nginx.conf

# copy the build output from the build-env.
COPY --from=build-env /app/build /usr/share/nginx/html

# expose port.
EXPOSE 80

# command to run nginx.
CMD ["nginx","-g","daemon off;"]
説明を開きます。
要素 説明
FROM node:lts-bookworm-slim as build-env ベースイメージを node:lts-bookworm-slim に設定します。このイメージは Node.js の環境を提供します。
WORKDIR /app ワーキングディレクトリを /app に設定します。
COPY package.json package-lock.json /app/ アプリケーションの依存関係をインストールするために package.json と package-lock.json をワーキングディレクトリにコピーします。
COPY webpack.config.js /app/ Webpack の設定ファイル webpack.config.js をワーキングディレクトリにコピーします。
COPY src /app/src アプリケーションのソースコードが含まれる src ディレクトリ内のファイルを /app/src にコピーします。
ARG API_URL ビルド時に渡される API_URL という名前の引数を定義します。この引数はビルド時に変数を渡すために使用されます。
ENV API_URL=$API_URL ビルドステージ内で定義された API_URL の値をコンテナ内の環境変数 API_URL に設定します。
RUN npm install && npm run build 依存関係をインストールしてアプリケーションをビルドします。この段階で先に定義した API_URL がアプリケーション内に埋め込まれます
FROM nginx:bookworm 2つ目のビルドステージを始めます。Nginx コンテナのベースイメージ nginx:bookworm に設定します。
COPY nginx.conf /etc/nginx/nginx.conf Nginx の設定ファイル nginx.conf を /etc/nginx/ にをコピーします。
COPY --from=build-env /app/build /usr/share/nginx/html build-env ステージからビルドされた結果を Nginx コンテナ内の /usr/share/nginx/html にコピーします。これにより、Nginx が静的ファイルを提供できるようになります。
EXPOSE 80 コンテナ内のポート 80 をエクスポートします。
CMD ["nginx","-g","daemon off;"] コンテナが起動した際に実行されるコマンドを指定します。ここでは Nginx をバックグラウンドで実行するコマンドを設定しています。

Docker デーモンを起動します。

$ sudo service docker start
 * Starting Docker: docker    [ OK ]

Docker 環境をお持ちでない場合は、以下の関連記事から Docker Engine のインストール手順をご確認いただけます。

コンテナイメージをビルドします。

$ docker build \
    --no-cache \
    --build-arg API_URL=http://localhost:5000 \
    --tag app-todo-vanillajs:latest .

コンテナをビルドする際に、API サービスの URL確定されている必要があります。

コンテナイメージを確認します。

$ docker images | grep app-todo-vanillajs
app-todo-vanillajs   latest      088ccd5bc579    7 seconds ago    187MB

ここまでの手順で、ローカル環境の Docker にアプリのカスタムコンテナイメージをビルドすることができました。

コンテナを起動

ローカルでコンテナを起動します。
※ コンテナを停止するときは ctrl + C を押します。

コンテナ間通信するために net-todo という Docker ネットワークを予め作成しています。ご注意ください。

$ docker run --rm \
    --publish 8000:80 \
    --name app-local \
    --net net-todo \
    app-todo-vanillajs

ここまでの手順で、ローカル環境の Docker でアプリのカスタムコンテナを起動することができました。

コンテナの動作確認

Web ブラウザで確認します。

$ wslview http://localhost:8000

image.png

コンテナの状態を確認してみます。

$ docker ps
CONTAINER ID   IMAGE                  COMMAND                   CREATED          STATUS          PORTS                                                  NAMES
e6c47d6ebd59   app-todo-vanillajs     "/docker-entrypoint.…"   48 minutes ago   Up 48 minutes   0.0.0.0:8000->80/tcp, :::8000->80/tcp                  app-local
d4704a13c409   api-todo-aspnet-core   "dotnet WebApp.dll"       5 hours ago      Up 5 hours      0.0.0.0:5000->80/tcp, :::5000->80/tcp                  api-local
63a3acec271c   mysql-base             "docker-entrypoint.s…"   2 days ago       Up 6 hours      0.0.0.0:3306->3306/tcp, :::3306->3306/tcp, 33060/tcp   mysql-todo

ここまでの手順で、カスタムコンテナとして起動した Todo アプリを操作することができました。

コンテナに接続

別ターミナルからコンテナに接続します。

$ docker exec -it app-local /bin/bash

コンテナに接続後にディレクトリを確認します。
※ コンテナから出るときは ctrl + D を押します。

# pwd
/
# cd /usr/share/nginx/html
# ls -lah
total 32K
drwxr-xr-x 1 root root 4.0K Aug 25 03:08 .
drwxr-xr-x 1 root root 4.0K Aug 16 09:50 ..
-rw-r--r-- 1 root root  497 Aug 15 17:03 50x.html
-rw-r--r-- 1 root root 9.8K Aug 25 03:08 bundle.js
-rw-r--r-- 1 root root  532 Aug 25 03:08 index.html

top コマンドで状況を確認します。

# apt update
# apt install procps
# top
top - 05:07:27 up  5:01,  0 user,  load average: 0.03, 0.12, 0.10
Tasks:   6 total,   1 running,   5 sleeping,   0 stopped,   0 zombie
%Cpu(s):  1.2 us,  0.4 sy,  0.0 ni, 98.3 id,  0.0 wa,  0.0 hi,  0.1 si,  0.0 st
MiB Mem :   7897.1 total,   1809.6 free,   3405.9 used,   3199.4 buff/cache
MiB Swap:   2048.0 total,   2048.0 free,      0.0 used.   4491.2 avail Mem

  PID USER      PR  NI    VIRT    RES    SHR S  %CPU  %MEM     TIME+ COMMAND
    1 root      20   0   11380   7132   6012 S   0.0   0.1   0:00.03 nginx
   29 nginx     20   0   11540   2592   1320 S   0.0   0.0   0:00.00 nginx
   30 nginx     20   0   11540   2592   1320 S   0.0   0.0   0:00.00 nginx
   31 nginx     20   0   11540   2592   1320 S   0.0   0.0   0:00.00 nginx
   32 root      20   0    4188   3428   2920 S   0.0   0.0   0:00.02 bash
  222 root      20   0    8632   4656   2772 R   0.0   0.1   0:00.01 top

コンテナの情報を表示してみます。

# cat /etc/*-release
PRETTY_NAME="Debian GNU/Linux 12 (bookworm)"
NAME="Debian GNU/Linux"
VERSION_ID="12"
VERSION="12 (bookworm)"
VERSION_CODENAME=bookworm
ID=debian
HOME_URL="https://www.debian.org/"
SUPPORT_URL="https://www.debian.org/support"
BUG_REPORT_URL="https://bugs.debian.org/"

このコンテナは Debian GNU/Linux をベースに作成されています。つまり、Debian GNU/Linux と同じように扱うことができます。

まとめ

WSL Ubuntu の Docker 環境で、JavaScript Vanilla JS の最小フロントエンドを実装することができました。

この記事の実装例は一つのアプローチに過ぎず、必ずしも正しい方法とは限りません。他にも多様な方法がありますので、さまざまな情報を照らし合わせて検討してみてください。

どうでしたか? WSL Ubuntu で、JavaScript Vanilla JS アプリケーションを手軽に起動することができます。ぜひお試しください。今後も JavaScript の開発環境などを紹介していきますので、ぜひお楽しみにしてください。

推奨コンテンツ

0
1
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
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?