メインコンテンツまでスキップ

Express によるサーバー構築

ウェブサイトが動作する仕組み

「Web プログラミングの基礎を学ぼう」 の章では、ウェブサイトを表示するために HTML ファイルと JavaScript ファイルを作成し、ブラウザから開きました。 しかし、一般的なウェブサイトを閲覧する際は、HTML ファイルや JavaScript ファイルの存在を意識することはありません。 これは、Web では、通常インターネットを介してデータをやり取りするためです。

インターネットを人間が直接利用することはできないので、何らかのコンピューターを使用しなければなりません。 このとき、

  • クライアント: サービスを利用する側のコンピューターや、その上で直接通信を担うソフトウェア
  • サーバー: サービスを提供する側のコンピューターや、その上で直接通信を担うソフトウェア

という二者の関係が発生します。また、その間で発生する通信を、その方向により

  • リクエスト: クライアントからサーバーに対する要求
  • レスポンス: リクエストに対するサーバーからクライアントへの応答

のように区別して呼びます。

Google Chrome や Safari に代表されるブラウザは、Web におけるクライアントソフトウェアの呼称です。 Web において、HTML、CSS、JavaScript は、サーバーがクライアントに対してレスポンスとして送信するデータの一部で、ブラウザによって解釈されて表示されます。

ここからは、Node.js を用いて、Web サーバーとして動作するソフトウェアを作成していきます。

クライアントとサーバー

ここまででの教材とのつながり

ブラウザは、サーバーと通信して取得した HTML、CSS、JavaScript データを解釈して表示するだけでなく、コンピューター上にファイルの形式で存在している HTML、CSS、JavaScript を表示することもできます。 ここまでの教材では、サーバーという概念の導入による複雑さを避けるため、この機能を用いて各言語の文法を学んできました。

Express パッケージを用いて Web サーバーを構築する

Express パッケージ を用いると、簡単に Web サーバーを構築できます。

まずは express パッケージを npm でインストールします。

npm install express

続いて、次のような main.mjs を作成しましょう。

main.mjs
import express from "express";
const app = express();

app.get("/", (request, response) => {
response.send(`Hello World! <a href="./lang/ja">日本語</a>`);
});
app.get("/lang/ja", (request, response) => {
response.send("こんにちは、世界!");
});

app.listen(3000);

ファイルを保存したら、作成したファイルを実行しましょう。

node main.mjs

ブラウザで http://localhost:3000/ にアクセスし、次の動作を確認してください。

  • Hello World! 日本語 が表示されること
  • 日本語 をクリックするとアドレスバーが http://localhost:3000/lang/ja に変化し こんにちは、世界! が表示されること
Web サーバーの停止

サーバーは、クライアントからのリクエストを待ち受けるため、管理者により指示されない限り終了しないソフトウェアとして動作します。 Express で構築したサーバーは、ターミナル上で control + C (macOS) / Ctrl + C (Windows) を押すことで停止することができます。

HTTP と URL

前項のプログラムが動作する仕組みを理解するためには、ブラウザとサーバーの間で行われるやり取りについて知る必要があります。 互いが好き勝手にデータを送受信しても、意味のあるやり取りは成立しません。 そこで、ブラウザとサーバーの間でデータを送受信する際には、一定の手順に従う必要があります。 この手順のことをプロトコルと呼び、Web の世界では、HTTP と呼ばれるプロトコルが用いられます。

ブラウザは、アドレスバーに URL が入力されると、その情報をもとに HTTP を用いてサーバーと通信します。 例えば、先ほどの例で使用した http://localhost:3000/lang/ja という URL は、次のように解釈されます。

http://プロトコルlocalhostドメイン:3000ポート/lang/jaパス

  • プロトコル: http または https のいずれかです。 いずれを指定しても使用されるプロトコルは HTTP ですが、https を指定した場合は通信内容が暗号化されます。 この例では http を用いていますが、インターネット上に公開するウェブサイトでは https を用いることが推奨されます。
  • ドメイン: 通信先のコンピューターを特定するための識別子です。 この例ではサーバーを自分のコンピューター上に構築しているため、自分自身を表す特殊なドメイン localhost を用いています。 example.comfoo.example.net は、実際のインターネット上で使われるドメイン名の例です。
  • ポート: 通信先のコンピューター上で、どのプログラムと通信するかを特定するための整数です。 ポートを変えることで、同じコンピューター上で複数のサーバーが通信を待ち受けることができます。 省略すると、プロトコル部分が http の場合は 80 番ポートが、https の場合は 443 番ポートが用いられます。
  • パス: クライアントがサーバーに対し、何を要求するかを指定するための文字列です。 Linux のファイルシステムで用いられるパスと同様に、意味的には / 記号によって階層化されますが、クライアントはサーバーに対してパスを文字列として送信するだけで、その内容はサーバーによって自由に解釈されます。

http://localhost:3000/lang/ja という URL は、HTTP プロトコルを暗号化せずに用いて、localhost というドメインの 3000 番ポートで待ち受けているサーバーに対して /lang/ja というパスを要求することを意味します。

このように、URL に含まれる情報のうち、プロトコル、ドメイン、ポートはネットワーク上で通信を行うために用いられる情報です。 残りのパス部分をどのように扱うかが、サーバー側のプログラムにおける主な関心事になります。

Hello World サーバーの動作

main.mjs のプログラムを参照しながら、ブラウザとサーバーの間でどのようなやり取りが行われているのかを確認しましょう。

main.mjs (再掲)
import express from "express";
const app = express();

app.get("/", (request, response) => {
response.send(`Hello World! <a href="./lang/ja">日本語</a>`);
});
app.get("/lang/ja", (request, response) => {
response.send("こんにちは、世界!");
});

app.listen(3000);

node main.mjs を実行したとき

  1. [2 行目] const app = express(); により、Express の Application クラスのインスタンスが作成されます。
  2. [4 行目] app.get("/", (request, response) => { ... }); で呼び出された get メソッドにより、/ というパスに対するリクエストを受けたときに実行される関数として、第 2 引数に指定された関数が登録されます。この時点では関数は Application インスタンスに登録されるのみで、実行はされません。
  3. [7 行目] 4 行目と同様に、/lang/ja というパスに対するリクエストを受けたときに実行される関数が登録されます。
  4. [11 行目] app.listen(3000); により、Express はリクエストを待ち受ける HTTP サーバーとして動作を開始します。

ブラウザの操作を行ったとき

  1. 利用者が、ブラウザのアドレスバーに http://localhost:3000/ という URL を入力します。
  2. ブラウザは、localhost:3000 で起動している Express のサーバーに対して、/ というパスに対する HTTP リクエストを送信します。
  3. Express は / に対する HTTP リクエストを受け取り、[4 行目] の関数を実行します。引数として、Express は次に示す 2 つのオブジェクトを生成して渡します。
    • 第 1 引数 (request): 受け取った HTTP リクエストに関する情報を Express が開発者にとって扱いやすい形にまとめた Request クラスのインスタンス。
    • 第 2 引数 (response): Express がクライアントに返す HTTP レスポンスを開発者が制御するための Response クラスのインスタンス。
  4. [5 行目]response.send(`...`); により、Express は `Hello World! <a href="./lang/ja">日本語</a>` という文字列をレスポンスとして送信します。
  5. ブラウザは、受け取ったレスポンスを HTML として解釈し、Hello World! という文字列と、日本語 というリンクを表示します。
  6. 利用者が、日本語 というリンクをクリックします。
  7. ブラウザは、クリックされたリンクの href 属性をもとに、アドレスバーの URL を更新し、ページ遷移を起こします。
    • href 属性にプロトコルを含む URL が指定されていれば、その URL に遷移します。
    • href 属性が相対パスの形式で指定されていれば、現在のパスを基準に Linux における相対パスと同様の方法で URL を更新します。この例では、更新前のパスが / で、href 属性には ./lang/ja が指定されているため、更新後のパスは /lang/ja になり、URL は http://localhost:3000/lang/ja に変化します。
  8. 2 から 5 までの手順が、/lang/ja というパスに対して行われます。

スライド 0

HTML に指定された外部リソースの取得

サーバーから返されたレスポンスが HTML である場合、ブラウザによってさらに別のリクエストが送信される場合があります。 例えば、HTML には CSS や JavaScript などの外部リソースを読み込むためのタグが存在します。 これらの外部リソースは、ブラウザが HTML を解釈する際に、それぞれに対するリクエストを送信して取得します。

次の例では、ブラウザは 2 つの HTTP リクエストを送信します。

  1. 利用者が、ブラウザのアドレスバーに http://localhost:3000/ という URL を入力します。
  2. ブラウザは、/ というパスに対する HTTP リクエストを送信します。
  3. サーバーは、 <script src="./script.js"></script> という文字列をレスポンスとして送信します。
  4. ブラウザは、受け取ったレスポンスを HTML として解釈し、script タグを発見します。
  5. ブラウザは、src 属性を相対パスとして解釈し、/script.js というパスに対して HTTP リクエストを送信します。
  6. サーバーは、document.write("Hello World!"); という文字列をレスポンスとして送信します。
  7. ブラウザは、受け取ったレスポンスを JavaScript として解釈し、Hello World! という文字列を表示します。
main.mjs
import express from "express";
const app = express();

app.get("/", (request, response) => {
response.send(`<script src="./script.js"></script>`);
});
app.get("/script.js", (request, response) => {
response.send(`document.write("Hello World!");`);
});

app.listen(3000);

静的ウェブサーバー

次の例では、//script.js/sub//sub/script.js へのリクエストについて、それぞれファイルから読み込んでレスポンスを送信しています。

main.mjs
import express from "express";
import { readFileSync } from "node:fs";
const app = express();

app.get("/", (request, response) => {
response.send(readFileSync("./static/index.html", "utf-8"));
});
app.get("/script.js", (request, response) => {
response.send(readFileSync("./static/script.js", "utf-8"));
});
app.get("/sub/", (request, response) => {
response.send(readFileSync("./static/sub/index.html", "utf-8"));
});
app.get("/sub/script.js", (request, response) => {
response.send(readFileSync("./static/sub/script.js", "utf-8"));
});

app.listen(3000);

express.static 関数を用いると、このような「リクエストを受け取ったら、そのパスに応じて適切なファイルを読み込んでレスポンスとして返す」という一連の動作を簡単に記述できます。

main.mjs
import express from "express";

const app = express();
app.use(express.static("static"));
app.listen(3000);

これにより、リクエストのパスをもとに、static フォルダ内の適切なファイルが自動的に配信されます。

index.html の省略

express.static を用いる場合、index.html は省略可能になります。つまり、/ へのリクエストで static/index.html が、/sub へのリクエストで static/sub/index.html にアクセスできるようになります。これは、Express や JavaScript に限ったことではなく、多くの Web サーバーの実装において、こういったルールが成り立ちます。

app.useexpress.static の実装

Express の use メソッド は、get メソッドと同様に、リクエストを受け取ったときに実行される関数を登録するためのメソッドです。 しかし、use メソッドは、登録された関数が全てのリクエストに対して実行される点が異なります。 express.static 関数は、関数を返す関数となっており、use メソッドに渡すことで、リクエストのパスに応じて適切なファイルを読み込んでレスポンスとして返す関数を登録することができます。

Express の Request オブジェクトのpath プロパティには、リクエストのパスが文字列として格納されています。 この値を用いて、express.static は適切なファイルを読み込みます。 次の例は、express.static 関数の簡易な実装例です。 このプログラムは動作はしますが、セキュリティ上の問題があるため、実際の Express の実装はもっと複雑になっています。

const app = express();
app.use((request, response) => {
response.send(readFileSync("./static" + request.path, "utf-8"));
});
app.listen(3000);

複雑なウェブページ

前項のプログラムを書き換えて、複雑な HTML を出力できるようにしてみましょう。

main.mjs
import express from "express";
const app = express();

const names = ["田中", "鈴木", "佐藤"];
app.get("/", (request, response) => {
response.send(`
<!doctype html>
<html lang="ja">
<head>
<meta charset="utf-8" />
<title>Title</title>
</head>
<body>
<ul>
${names.map((name) => `<li>${name}</li>`).join("")}
</ul>
</body>
</html>
`);
});

app.listen(3000);
Array#join

Array#join メソッドは、配列を指定した区切り文字で結合した文字列を返すメソッドです。

console.log(["Apple", "Banana", "Orange"].join("/")); // Apple/Banana/Orange

このようにテンプレートリテラルを用いることで、複雑なウェブページの内容を表すことができます。

String#replace メソッド

上記のようにテンプレートリテラルを使って HTML を作成することもできますが、HTML がさらに長くなったり、複雑なプログラムが必要になってきたりしたらこのまま続けていくのは難しそうです。

String#replace メソッドは、第 1 引数に与えられた文字列に一致する文字列を第 2 引数の文字列に置き換えた新しい文字列を返します。 String#replace メソッドは、最初に一致した場所のみを置き換えます。 一致するすべての場所を置き換えたい場合は、代わりに String#replaceAll メソッドを使います。

const base = "ABCBCC";
document.write(base.replace("BC", "EFG")); // AEFGBCC
document.write(base.replaceAll("C", "T")); // ABTBTT

String#replace メソッドを用いることで、長い HTML を見通しよく記述できるようになります。

index.html
<!doctype html>
<html lang="ja">
<head>
<meta charset="utf-8" />
<title>Title</title>
</head>
<body>
<ul>
<!-- users -->
</ul>
</body>
</html>
main.mjs
import express from "express";
import { readFileSync } from "node:fs";

const app = express();

const names = ["田中", "鈴木", "佐藤"];
app.get("/", (request, response) => {
const template = readFileSync("./index.html", "utf-8");
const html = template.replace(
"<!-- users -->",
names.map((name) => `<li>${name}</li>`).join(""),
);
response.send(html);
});

app.listen(3000);

リクエストを受け取ったとき、プログラムではまず readFileSync 関数を用いて index.html ファイルの内容を文字列として読み込んでいます。

次の行で String#replace メソッドを用いて、読み込んだ HTML ファイルの中の "<!-- users -->" を、 names を箇条書きにした文字列に置換します。

演習

訪問者カウンター

Express を用いて、あなたは n 人目のお客様です。 とレスポンスする Web サーバーを作成してください。n はアクセスされるたびに 1 ずつ増えるようにしてください。

解答例: 訪問者カウンター
main.mjs
import express from "express";
const app = express();

let count = 0;
app.get("/", (request, response) => {
count += 1;
response.send(`あなたは${count}人目のお客様です。`);
});

app.listen(3000);

サーバー側とクライアント側

(重要) アクセスされた時刻をウェブサーバー側で求めて表示するウェブサーバーと、ブラウザに求めさせるウェブサーバーをそれぞれ作成してください。また、この 2 つの違いは何でしょうか。どういった場合にどちらの手法を使うのが適切でしょうか。

解答例: サーバー側とクライアント側
app.get("/", (request, response) => {
response.send(`
<!doctype html>
<html lang="ja">
<head>
<meta charset="utf-8" />
<title>Title</title>
</head>
<body>
${
// この部分はサーバーサイドで実行されます
new Date().toString()
}
<script>
// この部分はクライアントサイドで実行されます
document.write(new Date().toString());
</script>
</body>
</html>
`);
});