おさらい
前回ついにデータベースをプログラム上から操作することができるようになりました。
sqlite3というモジュールを使って、queries.js
に定義したSQL文を実行することで、データベースを操作することができました。
ファイル分割をすることで適切なファイルに適切な処理を書き分けることができ、プログラムが見やすくなりました。
SQLインジェクションという脆弱性についても学び、SQL文を実行する際には、ユーザーからの入力をそのまま使わず、プレースホルダー(?
)を使って実行するようにしました。
HTMLがブラウザに表示される仕組み(HTTP)
以前の講義でHTMLを学んだ時に、それをブラウザで表示する方法として、htmlファイルを作りブラウザで開くという方法を学びました。 しかし実は、ブラウザはHTMLファイルを直接読み込んでいるわけではありません。これについて解説します。
HTTPとは
ブラウザがHTMLを読み込む際に使うのがHTTPというプロトコルです。皆さん聞いたことがあると思いますが、HTTPとはHyperText Transfer Protocolの略で、ハイパーテキストを転送するためのプロトコルです。
じつは、ブラウザはHTMLファイルを直接読み込んでいるのではなく、HTTPを使ってHTML形式の文字列をサーバーから取得しています。そして、その文字列をブラウザが解釈して、HTMLとして表示しているのです。
つまりファイルである必要はなく、HTMLだよっていう命令と、HTML形式の文字列が取得できれば、ブラウザはそれを表示することができるのです。
通信にはヘッダーとボディがあり、ヘッダーには通信に関する情報が、ボディには実際に取得したデータが入っています。
実験してみよう
実際に、ブラウザがHTMLファイルを読み込んでいるのではなく、HTTPを使ってHTML形式の文字列を取得していることを確認してみましょう。
まず、適当な場所にindex.html
を作成し、以下のように書きます。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>これはHTMLファイルです</title>
</head>
<body>
<h1>こんにちは、これはHTMLファイルです</h1>
</body>
</html>
ブラウザでindex.html
を開いて、表示されることを確認してください。
次に検証ツールからネットワークタブを開き、サイトをリロードしてください。
通信があったHTMLの要素をクリックし、Headers
タブをクリックすると、以下のような画面が表示されます。
ここのResponse Headers
のContent-Type
を見てください。text/html
となっていることがわかります。
HTTP通信には
ブラウザはHTTP通信のヘッダーを見て、Content-Type
がtext/html
であることを確認したら、ボディの内容(文字列)をHTMLとして解釈し表示しているのです。
ではわざとHTMLファイルを使わずに、HTTP通信のボディにHTML形式の文字列を入れてみましょう。
次に、server.js
をこれまた適当な場所に作成し、以下のように書きます。
const http = require('http');
const HTML = `
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>これはただの文字列です</title>
</head>
<body>
<h1>こんにちは、これはただの文字列です</h1>
</body>
</html>
`;
const server = http.createServer((req, res) => {
res.writeHead(200, {'Content-Type': 'text/html'});
res.end(HTML);
});
server.listen(3000);
そして、node server.js
としてサーバーを起動し、ブラウザでhttp://localhost:3000
にアクセスしてみましょう。
先ほどのHTMLファイルをブラウザで開いたときと同じように、ちゃんとHTMLとして表示されていることがわかります。
ちなみに、今回はContent-Type
をtext/html
にしていますが、text/plain
にすることもできます。
この場合、ブラウザはHTMLとして解釈せず、ただの文字列として表示します。
データベースを繋げてWebサイトを作ってみよう
今回はより簡単に実装ができるように、最近話題のフレームワークであるHonoを使ってWebサイトを作っていきます。
Honoのインストール
まずはHonoをインストールします。前回の講義でつかった例のプロジェクトのディレクトリに移動し、以下のコマンドを実行してください。
npm install hono @hono/node-server
Nodejs18が必要です
@hono/node-server
はNodejs18以上のバージョンが必要です。もしNodejs18以上のバージョンがインストールされていない場合は、以下のコマンドを実行してください。
sudo apt update
sudo apt autoremove nodejs
sudo apt remove nodejs
curl -fsSL https://deb.nodesource.com/setup_lts.x | sudo -E bash - && sudo apt-get install -y nodejs
でNodejs18をインストールしてください。
サーバーをたててみよう
次に、サーバーをたててみましょう。index.js
を以下のように書き換えてください。
const sqlite3 = require("sqlite3").verbose();
const queries = require("./queries");
const { serve } = require("@hono/node-server");
const { Hono } = require("hono");
const db = new sqlite3.Database("database.db");
db.serialize(() => {
db.run(queries.Tweets.createTable);
db.run(queries.Users.createTable);
db.run(queries.Users.create, 'りんご太郎', '[email protected]', '2022-08-15 00:00:00');
db.run(queries.Users.create, 'みかん次郎', '[email protected]', '2022-08-15 00:00:01');
db.run(queries.Users.create, 'ぶどう三郎', '[email protected]', '2022-08-15 00:00:02');
db.run(queries.Tweets.create, 'あけおめ!', 3, '2023-01-01 00:00:00');
db.run(queries.Tweets.create, '今年もよろしくお願いします!', 2, '2023-01-01 00:00:01');
db.run(queries.Tweets.create, '今年こそは痩せるぞ!', 1, '2023-01-01 00:00:02');
});
const app = new Hono();
app.get("/", (c) => {
return c.text("Hello World!");
});
serve(app);
process.stdin.on("data", (data) => {
if (data.toString().trim() === "q") {
db.close();
process.exit();
}
});
そして、node index.js
としてサーバーを起動し、ブラウザでhttp://localhost:3000
にアクセスしてみましょう。
c.text
はContent-Type
をtext/plain
にして、引数の文字列を返す関数です。
app.get
は、GET
リクエストが第一引数のパスに来たときに、第二引数の関数を実行するという関数です。
serve
は、第一引数のHonoインスタンスを使ってサーバーを起動する関数です。
ブラウザにHello World!
と表示されていれば成功です。
ネットワークタブでもContent-Type
がtext/plain
であることがわかります。
いままでプログラムをCtrl+C
で終了していましたが、今回はq
を入力すると終了するようにしています。こうすることで、サーバーを終了するときに、確実にデータベースを閉じることができます。
コミット[サーバーを立てるプログラムを作成]
git add .
git commit -m "サーバーを立てるプログラムを作成"
データベースの情報を表示してみよう
次に、データベースの情報を表示してみましょう。index.js
を以下のように書き換えてください。
const sqlite3 = require("sqlite3").verbose();
const queries = require("./queries");
const { serve } = require("@hono/node-server");
const { Hono } = require("hono");
const db = new sqlite3.Database("database.db");
db.serialize(() => {
db.run(queries.Tweets.createTable);
db.run(queries.Users.createTable);
db.run(queries.Users.create, 'りんご太郎', '[email protected]', '2022-08-15 00:00:00');
db.run(queries.Users.create, 'みかん次郎', '[email protected]', '2022-08-15 00:00:01');
db.run(queries.Users.create, 'ぶどう三郎', '[email protected]', '2022-08-15 00:00:02');
db.run(queries.Tweets.create, 'あけおめ!', 3, '2023-01-01 00:00:00');
db.run(queries.Tweets.create, '今年もよろしくお願いします!', 2, '2023-01-01 00:00:01');
db.run(queries.Tweets.create, '今年こそは痩せるぞ!', 1, '2023-01-01 00:00:02');
});
const app = new Hono();
app.get("/", async (c) => {
const tweets = await new Promise((resolve) => {
db.all(queries.Tweets.findAll, (err, rows) => {
resolve(rows);
});
});
return c.json(tweets);
});
serve(app);
process.stdin.on("data", (data) => {
if (data.toString().trim() === "q") {
db.close();
process.exit();
}
});
ちょっと複雑な処理が出てきましたね。
db.all
は、SQLを実行して結果を全て取得する関数でしたね。
db.all
はコールバック関数を第二引数に取り、第一引数のSQLを実行した結果を第二引数のコールバック関数の第二引数に渡します。
コールバック関数の第一引数にはエラーが渡されるので、エラーがなければrows
に結果が渡されるという仕組みです。
(本当はエラー処理をちゃんとするべきですが、今回は省略しています)
db.all
は同期関数(コールバック関数を第二引数に取る関数)なので、Promise
を使って非同期関数に変換して、取得が終わるまで待つという処理をしています。
c.json
はContent-Type
をapplication/json
にして、引数のオブジェクトをJSON形式の文字列に変換して返す関数です。
今回、findAll
クエリで帰ってくるデータはオブジェクトなので、c.text
を使って返そうとすると、[object Object]
という文字列が返ってきてしまいます。
(オブジェクトを無理やり文字列に変換するとこのような文字列になります)
なので、c.json
を使ってJSON形式の文字列に変換して返しているというわけです。
ブラウザでhttp://localhost:3000
にアクセスしてみましょう。
データベースの情報がJSON形式で表示されていることがわかりますね?
コミット[データベースの情報をサーバーから返すプログラムを作成]
git add .
git commit -m "データベースの情報をサーバーから返すプログラムを作成"
HTMLを表示してみよう
次に、HTMLを表示してみましょう。index.js
を以下のように書き換えてください。
const sqlite3 = require("sqlite3").verbose();
const queries = require("./queries");
const { serve } = require("@hono/node-server");
const { Hono } = require("hono");
const db = new sqlite3.Database("database.db");
db.serialize(() => {
db.run(queries.Tweets.createTable);
db.run(queries.Users.createTable);
db.run(queries.Users.create, 'りんご太郎', '[email protected]', '2022-08-15 00:00:00');
db.run(queries.Users.create, 'みかん次郎', '[email protected]', '2022-08-15 00:00:01');
db.run(queries.Users.create, 'ぶどう三郎', '[email protected]', '2022-08-15 00:00:02');
db.run(queries.Tweets.create, 'あけおめ!', 3, '2023-01-01 00:00:00');
db.run(queries.Tweets.create, '今年もよろしくお願いします!', 2, '2023-01-01 00:00:01');
db.run(queries.Tweets.create, '今年こそは痩せるぞ!', 1, '2023-01-01 00:00:02');
});
const HTML = (body) => `
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>これはただの文字列です</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body>
${body}
</body>
</html>
`;
const app = new Hono();
app.get("/", async (c) => {
const tweets = await new Promise((resolve) => {
db.all(queries.Tweets.findAll, (err, rows) => {
resolve(rows);
});
});
const response = HTML(`
<h1 class="title">ツイート一覧</h1>
`);
return c.html(response);
});
serve(app);
process.stdin.on("data", (data) => {
if (data.toString().trim() === "q") {
db.close();
process.exit();
}
});
HTML
という関数を定義して、引数の文字列をHTML形式の文字列に変換して返すようにしています。
ブラウザでhttp://localhost:3000
にアクセスしてみましょう。
すると大きく「ツイート一覧」と書かれた文字が表示されていることがわかります。
実際に検証ツールのNetwork
タブから通信の内容を見てみると、Content-Type
がtext/html
になっていること、そしてタイトルの内容(引数に書いた内容)が埋め込まれたHTML
関数の戻り値が返ってきていることがわかります。
コミット[HTMLをサーバーから返すプログラムを作成]
git add .
git commit -m "HTMLをサーバーから返すプログラムを作成"
ツイートの内容も表示してみよう
では、ツイートの内容も表示してみましょう。index.js
を以下のように書き換えてください。
const sqlite3 = require("sqlite3").verbose();
const queries = require("./queries");
const { serve } = require("@hono/node-server");
const { Hono } = require("hono");
const db = new sqlite3.Database("database.db");
db.serialize(() => {
db.run(queries.Tweets.createTable);
db.run(queries.Users.createTable);
db.run(queries.Users.create, 'りんご太郎', '[email protected]', '2022-08-15 00:00:00');
db.run(queries.Users.create, 'みかん次郎', '[email protected]', '2022-08-15 00:00:01');
db.run(queries.Users.create, 'ぶどう三郎', '[email protected]', '2022-08-15 00:00:02');
db.run(queries.Tweets.create, 'あけおめ!', 3, '2023-01-01 00:00:00');
db.run(queries.Tweets.create, '今年もよろしくお願いします!', 2, '2023-01-01 00:00:01');
db.run(queries.Tweets.create, '今年こそは痩せるぞ!', 1, '2023-01-01 00:00:02');
});
const HTML = (body) => `
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>これはただの文字列です</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body>
${body}
</body>
</html>
`;
const app = new Hono();
app.get("/", async (c) => {
const tweets = await new Promise((resolve) => {
db.all(queries.Tweets.findAll, (err, rows) => {
resolve(rows);
});
});
const tweetList = tweets.map((tweet) => `<div class="tweet">${tweet.content}</div>`).join("\n");
const response = HTML(`
<h1 class="title">ツイート一覧</h1>
<div class="tweet-list">
${tweetList}
</div>
`);
return c.html(response);
});
serve(app);
process.stdin.on("data", (data) => {
if (data.toString().trim() === "q") {
db.close();
process.exit();
}
});
tweetList
という変数に、それぞれのツイートの内容を<div class="tweet">
で囲んだ文字列を配列にして格納しています。
もし気になる人はconsole.log(tweetList)
をしてみてください。
そして、tweetList
をHTML
関数の引数に渡しています。
ブラウザでhttp://localhost:3000
にアクセスしてみましょう。
ツイートの内容が表示されていることがわかりますね?
コミット[ツイートの内容をHTMLで表示するプログラムを作成]
git add .
git commit -m "ツイートの内容をHTMLで表示するプログラムを作成"
CSSを適用してみよう
表示するだけじゃ面白くないので、デザインを適用してみましょう。
まず、static/style.css
を作成します。
static
ディレクトリは静的ファイルを置くディレクトリです。作ってください。
静的ファイルとは、サーバーが動作している間に変更されないファイルのことです。
今回はCSSファイルを置くために使います。
body {
margin: 0;
background-color: #edf3f6;
}
.title {
font-size: 60px;
text-align: center;
}
.tweet-list {
display: flex;
flex-direction: column;
gap: 16px;
max-width: 768px;
padding: 16px;
box-sizing: border-box;
width: 100%;
margin: 0 auto;
}
.tweet {
display: flex;
flex-direction: column;
gap: 8px;
padding: 16px;
border-radius: 8px;
background-color: #fff;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.25);
}
そして、index.js
にこのような追加をします
const sqlite3 = require("sqlite3").verbose();
const queries = require("./queries");
const { serve } = require("@hono/node-server");
const { serveStatic } = require("@hono/node-server/serve-static");
const { Hono } = require("hono");
const db = new sqlite3.Database("database.db");
db.serialize(() => {
db.run(queries.Tweets.createTable);
db.run(queries.Users.createTable);
db.run(queries.Users.create, 'りんご太郎', '[email protected]', '2022-08-15 00:00:00');
db.run(queries.Users.create, 'みかん次郎', '[email protected]', '2022-08-15 00:00:01');
db.run(queries.Users.create, 'ぶどう三郎', '[email protected]', '2022-08-15 00:00:02');
db.run(queries.Tweets.create, 'あけおめ!', 3, '2023-01-01 00:00:00');
db.run(queries.Tweets.create, '今年もよろしくお願いします!', 2, '2023-01-01 00:00:01');
db.run(queries.Tweets.create, '今年こそは痩せるぞ!', 1, '2023-01-01 00:00:02');
});
const HTML = (body) => `
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>これはただの文字列です</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="/static/style.css">
</head>
<body>
${body}
</body>
</html>
`;
const app = new Hono();
app.get("/", async (c) => {
const tweets = await new Promise((resolve) => {
db.all(queries.Tweets.findAll, (err, rows) => {
resolve(rows);
});
});
const tweetList = tweets.map((tweet) => `<div class="tweet">${tweet.content}</div>`).join("\n");
const response = HTML(`
<h1 class="title">ツイート一覧</h1>
<div class="tweet-list">
${tweetList}
</div>
`);
return c.html(response);
});
app.use("/static/*", serveStatic({ root: "./" }));
serve(app);
process.stdin.on("data", (data) => {
if (data.toString().trim() === "q") {
db.close();
process.exit();
}
});
app.use
は、第一引数のパスに来たリクエストに対して、第二引数のプラグインを実行するという関数です。
serveStatic
は、第一引数のオプションを使って、静的ファイルを返すプラグインです。
root
オプションは、静的ファイルを置くディレクトリを指定します。
app.use
を使って、/static/*
に来たリクエストに対して、静的ファイルを返すプラグインを実行するようにしています。
ブラウザでhttp://localhost:3000
にアクセスしてみましょう。
なんとなくデザインが適用されていれば成功です。
コミット[サイトにCSSを適用するように拡張]
git add .
git commit -m "サイトにCSSを適用するように拡張"
SSR
今までやってきた、サーバーで取得したデータをHTMLに埋め込んで返すという方法は、サーバーサイドレンダリングと呼ばれます。以降SSRと呼びます。
SSRは、サーバーでHTMLを生成してからブラウザに返すので、ブラウザはHTMLを受け取ってすぐに表示することができます。
以前、このSSRが主流でした。たとえば私たちが使っているWebClassもSSRで動いています。
この方法は、前回講義で説明した3層アーキテクチャには従っていません。というのもアプリケーション層とプレゼンテーション層が混ざってしまっているからです。
これを避ける方法が二つあって
- クライアントサイドレンダリング(CSR)にする
- プレゼンテーション層を別のサーバーにする(アプリケーションサーバーとは別のSSR用のサーバーを用意する)
最近は2つ目の方法が主流です。Next.jsやNuxt.jsなどのフレームワークは、この方法を採用しています。
しかし、こちらはとてもハイコンテキストで習得に技術力と時間を要するので、今回はやりません。 代わりに、1つ目の方法であるCSRを次回からやっていきます。