今回のインターンシップではブラウザ上で動作する Web アプリを作成します。
フロントエンドは HTML / CSS / JavaScript、バックエンドは JavaScript を使って作成します。
この資料をもとに Web アプリの基本について学び、作成した Web アプリを提出してください。
「Step 13. Deno Deploy にデプロイ 」や作成した Web アプリの提出に GitHub のアカウントが必要になりますので、まだアカウントをお持ちでない方は作成をお願いします。
バックエンドは Deno を使ってサーバーを立てます。
Deno は JavaScript の実行環境で、JavaScript でサーバーのプログラムを書きます。
フロントエンドで JavaScript を使った経験がある方は多いと思いますが、JavaScript でサーバーも作成できることがメリットです。
JavaScript でサーバーというと Node.js が挙げられますが、作者が Node.js の反省から Deno を作ったので、より便利なものと思ってもらって大丈夫です!(まだライブラリは少ないですが・・・)
公式サイトの説明に従って Deno をインストールしてください。
Installation の項目にあるスクリプトを自分の環境に合わせて実行するだけでインストールできるかと思います。
インストールが完了したら、以下のコマンドを実行してみましょう。
deno run https://deno.land/std/examples/welcome.ts |
以下の文字が表示されれば OK です。
Welcome to Deno! |
Deno をインストール済みの方は、以下のコマンドを実行して最新版に更新しておきましょう。
deno upgrade |
Hello World のプログラムを書いて実行してみましょう。
hello.js ファイルに以下のプログラムを書いて保存します。
hello.js |
console.log("Hello World"); |
プログラムを保存したら、以下のコマンドで実行します。
deno run hello.js |
Hello World の文字が表示されれば OK です。
Hello World |
プログラムは好きなエディタで作成して構いませんが、Visual Studio Code がインストールされている環境であれば、以下のコマンドでファイルを作成して編集を開始することができて便利です。
code hello.js |
また、Visual Studio Code に拡張機能を追加することで、より快適に開発することができます。
拡張機能で「Deno」をインストールします。
コマンドパレット(Ctrl+Shift+P)で「Deno: Initialize Workspace Configuration」を実行します。
.vscode/setting.json が作成され、Deno 関係のコードが補完されれば OK です。
次に HTTP サーバーを立ち上げてみます。
HTTP サーバーは、その名の通り HTTP(Hypertext Transfer Protocol)のプロトコルでブラウザと通信するサーバーです。HTTP サーバーとブラウザが様々なデータをやり取りすることで Web アプリが動作します。
server.js ファイルに以下のプログラムを書いて保存します。
server.js |
import { serve } from "https://deno.land/std@0.138.0/http/server.ts" console.log("Listening on http://localhost:8000"); serve(req => { return new Response("Hello World"); }); |
プログラムを保存したら、以下のコマンドで実行します。
deno run --allow-net server.js |
–allow-net のオプションが付いてないと Deno のプログラムがネットワークに接続することができずエラーが発生します。
コンソールに以下の文字が表示されていることが確認できたら、Chrome などのブラウザで http://localhost:8000 にアクセスしてみましょう。
Listening on http://localhost:8000 |
ブラウザで「Hello World」が表示されれば OK です。
サーバーのプログラムは自動では終了しないので、 Ctrl + C で終了します。
HTTP サーバーにアクセス数をカウントする変数を追加してみましょう。
server.js ファイルを以下の内容で編集します。
(赤字の部分が変更箇所です)
server.js |
import { serve } from "https://deno.land/std@0.138.0/http/server.ts" let count = 0; console.log("Listening on http://localhost:8000"); serve(req => { count++; return new Response(`Count: ${count}`); }); |
プログラムを保存したら、以下のコマンドでサーバーを起動しましょう。
deno run --allow-net --watch server.js |
--watch のオプションを付けると、server.js ファイルの保存時に自動でプログラムを再読み込みしてくれるので、いちいちコマンドを再入力する必要がなくなり便利です。
ブラウザで http://localhost:8000 にアクセスするとカウントが表示され、ページを再読込みするたびにカウントが増えていけば OK です。
※ブラウザが勝手にファビコン(favicon.ico)にアクセスするため、再読込みすると 2ずつカウントが増えることがあります。
ブラウザで HTML を表示させてみます。
server.js ファイルを以下の内容で編集して、サーバーからレスポンスとして、<h1> タグを含む HTML を返すようにしてみましょう。
server.js |
import { serve } from "https://deno.land/std@0.138.0/http/server.ts"; console.log("Listening on http://localhost:8000"); serve((req) => { return new Response("<h1>見出しです</h1>"); }); |
ブラウザで http://localhost:8000 にアクセスすると、サーバーから返した文字列がそのまま表示されると思います。
これはブラウザがサーバーから返ってきたデータが HTML だと理解できなかったからです。
データが HTML であることがわかるように、以下のように Content-Type ヘッダを返すようにプログラムを変更します。
server.js |
import { serve } from "https://deno.land/std@0.138.0/http/server.ts"; console.log("Listening on http://localhost:8000"); serve((req) => { return new Response("<h1>見出しです</h1>", { headers: { "Content-Type": "text/html; charset=utf-8" } }); }); |
ブラウザを再読込みして、「見出し」と大きく表示されれば OK です。
このようにサーバーからは返すデータに合わせて、Content-Type ヘッダを設定してやる必要があります。
代表的なデータとして以下のようなものがあります。
text/html | HTML ファイル |
text/css | CSS ファイル |
text/javascript | JavaScript ファイル |
application/json | JSON ファイル |
image/jpeg | 画像(JPEG)ファイル |
image/png | 画像(PNG)ファイル |
Step 5 では HTML をサーバーのプログラムで文字列として記述しましたが、別のファイルとして保存したものを読み込むようにしてみます。
まず、HTML ファイルを作成しましょう。
public フォルダを作成し、その中に index.html ファイルを以下の内容で保存します。
public/index.html |
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> </head> <body> <h1>見出しです</h1> </body> </html> |
次に server.js をファイルを読み込むように変更します。
server.js |
import { serve } from "https://deno.land/std@0.138.0/http/server.ts"; console.log("Listening on http://localhost:8000"); serve(async (req) => { return new Response(await Deno.readTextFile("./public/index.html"), { headers: { "Content-Type": "text/html; charset=utf-8" }, }); }); |
このプログラムの実行には Deno のプログラムからファイルの読み込みを許可するために --allow-read のオプションが必要です。
deno run --allow-net --allow-read --watch server.js |
ブラウザで http://localhost:8000 にアクセスしてページが表示されれば OK です。
CSS ファイルを追加して装飾してみます。
public フォルダの中に styles.css ファイルを以下の内容で作成します。
public/styles.css |
body { background: skyblue; } |
index.html で CSS ファイルを参照するように変更します。
public/index.html |
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <link rel="stylesheet" href="styles.css"> </head> <body> <h1>見出しです</h1> </body> </html> |
server.js はリクエストされた URL に応じて HTML もしくは CSS ファイルを返すように変更します。
server.js |
import { serve } from "https://deno.land/std@0.138.0/http/server.ts"; console.log("Listening on http://localhost:8000"); serve(async (req) => { const pathname = new URL(req.url).pathname; console.log(pathname); if (pathname === "/styles.css") { return new Response(await Deno.readTextFile("./public/styles.css"), { headers: { "Content-Type": "text/css; charset=utf-8" }, }); } return new Response(await Deno.readTextFile("./public/index.html"), { headers: { "Content-Type": "text/html; charset=utf-8" }, }); }); |
ブラウザを再読込みして青い背景になれば OK です。
Step 6 で HTML と CSS ファイルを返すサーバーを作りましたが、ファイルを追加するたびにサーバーのプログラムを変更する必要があるのは非効率です。
public フォルダに保存されている任意のファイルを取得する、いわゆるファイルサーバーとして動作するようにします。
server.js ファイルを以下の内容で編集します。
server.js |
import { serve } from "https://deno.land/std@0.138.0/http/server.ts"; import { serveDir } from "https://deno.land/std@0.138.0/http/file_server.ts"; console.log("Listening on http://localhost:8000"); serve(async (req) => { const pathname = new URL(req.url).pathname; console.log(pathname); return serveDir(req, { fsRoot: "public", urlRoot: "", showDirListing: true, enableCors: true, }); }); |
Deno の標準ライブラリを使って簡単にファイルサーバーを構築することができました。
これだけで public フォルダの任意のファイルが、拡張子に合わせて適切な Content-Type ヘッダがセットされて返ってきます。
これから「しりとり」をする Web アプリを作成していきます。
最低限の機能として、サーバーでしりとりの前の単語を保持するようにして、ブラウザからそれに繋がる次の単語を送って更新できるようにします。
まず、サーバーで前の単語を記録する変数を追加し、それを取得する API を用意しましょう。
server.js |
import { serve } from "https://deno.land/std@0.138.0/http/server.ts"; import { serveDir } from "https://deno.land/std@0.138.0/http/file_server.ts"; let previousWord = "しりとり"; console.log("Listening on http://localhost:8000"); serve(async (req) => { const pathname = new URL(req.url).pathname; console.log(pathname); if (pathname === "/shiritori") { return new Response(previousWord); } return serveDir(req, { fsRoot: "public", urlRoot: "", showDirListing: true, enableCors: true, }); }); |
サーバーを起動し、ブラウザで http://localhost:8000/shiritori にアクセスして、「しりとり」の文字が表示されれば OK です。
次に、ブラウザから送られてきた次の単語を受け取る部分を作成します。
ブラウザから以下の形式の JSON で次の単語を送られてくることとします。
{ "nextWord": "次の単語" } |
server.js |
import { serve } from "https://deno.land/std@0.138.0/http/server.ts"; import { serveDir } from "https://deno.land/std@0.138.0/http/file_server.ts"; let previousWord = "しりとり"; console.log("Listening on http://localhost:8000"); serve(async (req) => { const pathname = new URL(req.url).pathname; console.log(pathname); if (req.method === "GET" && pathname === "/shiritori") { return new Response(previousWord); } if (req.method === "POST" && pathname === "/shiritori") { const requestJson = await req.json(); const nextWord = requestJson.nextWord; previousWord = nextWord; return new Response(previousWord); } return serveDir(req, { fsRoot: "public", urlRoot: "", showDirListing: true, enableCors: true, }); }); |
同じ /shiritori のパスに対して、HTTP のリクエストメソッドの GET と POST で処理を分けるようにしました。
POST メソッドで JSON を送る部分の動作確認はブラウザだけではできないので、次はフロントエンドを作成していきます。
フロントエンドのプログラムとして、ブラウザで JavaScript を実行してみましょう。
public/index.html |
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <link rel="stylesheet" href="styles.css"> </head> <body> <h1>見出しです</h1> <script type="module"> alert("Hello World"); </script> </body> </html> |
ブラウザで http://localhost:8000 にアクセスして、ダイアログで「Hello World」と表示されれば OK です。
さきほど作成したサーバーの API を使って、しりとりの前の単語を取得します。
HTTP 通信には Fetch API を使用します。
public/index.html |
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <link rel="stylesheet" href="styles.css"> </head> <body> <h1>しりとり</h1> <script type="module"> window.onload = async (event) => { const response = await fetch("/shiritori"); const previousWord = await response.text(); alert(previousWord); }; </script> </body> </html> |
ブラウザでページを再読込みして、ダイアログで「しりとり」と表示されれば OK です。
サーバーから取得した前の単語を、DOM 操作してページの要素として表示してみましょう。
public/index.html |
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <link rel="stylesheet" href="styles.css"> </head> <body> <h1>しりとり</h1> <p id="previousWord"></p> <script type="module"> window.onload = async (event) => { const response = await fetch("/shiritori"); const previousWord = await response.text(); const para = document.querySelector("#previousWord"); para.innerText = `前の単語:${previousWord}`; }; </script> </body> </html> |
ブラウザでページを再読込みして、「前の単語:しりとり」と表示されれば OK です。
次は Fetch API を使って POST で JSON を送ってみましょう。
public/index.html |
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <link rel="stylesheet" href="styles.css"> </head> <body> <h1>しりとり</h1> <p id="previousWord"></p> <script type="module"> window.onload = async (event) => { await fetch("/shiritori", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ nextWord: "りんご" }) }); const response = await fetch("/shiritori"); const previousWord = await response.text(); const para = document.querySelector("#previousWord"); para.innerText = `前の単語:${previousWord}`; }; </script> </body> </html> |
ブラウザでページを再読込みして、「前の単語:りんご」と表示されれば OK です。
表示されない場合は、サーバーのプログラムがおかしいかもしれないので確認しましょう。
テキストフィールドやボタンを追加して、任意の単語をサーバーに送れるようにします。
public/index.html |
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <link rel="stylesheet" href="styles.css"> </head> <body> <h1>しりとり</h1> <p id="previousWord"></p> <input id="nextWordInput" type="text" /> <button id="nextWordSendButton">送信</button> <script type="module"> window.onload = async (event) => { const response = await fetch("/shiritori"); const previousWord = await response.text(); const para = document.querySelector("#previousWord"); para.innerText = `前の単語:${previousWord}`; }; document.querySelector("#nextWordSendButton").onclick = async (event) => { const nextWord = document.querySelector("#nextWordInput").value; const response = await fetch("/shiritori", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ nextWord }) }); const previousWord = await response.text(); const para = document.querySelector("#previousWord"); para.innerText = `前の単語:${previousWord}`; }; </script> </body> </html> |
テキストフィールドに単語を入力し送信ボタンをクリックして、ページの前の単語の表示が更新されれば OK です。
どんな単語を送信しても更新できてしまうので、しりとりとして成立するように入力チェックを追加します。
サーバーで、ブラウザから送られた単語が前の単語の最後の文字で始まっているかどうかチェックして、おかしければ更新しないようにします。
また、HTTP のステータスコードを 400 にして、エラーメッセージを返すようにします。
server.js |
import { serve } from "https://deno.land/std@0.138.0/http/server.ts"; import { serveDir } from "https://deno.land/std@0.138.0/http/file_server.ts"; let previousWord = "しりとり"; console.log("Listening on http://localhost:8000"); serve(async (req) => { const pathname = new URL(req.url).pathname; if (req.method === "GET" && pathname === "/shiritori") { return new Response(previousWord); } if (req.method === "POST" && pathname === "/shiritori") { const requestJson = await req.json(); const nextWord = requestJson.nextWord; if ( nextWord.length > 0 && previousWord.charAt(previousWord.length - 1) !== nextWord.charAt(0) ) { return new Response("前の単語に続いていません。", { status: 400 }); } previousWord = nextWord; return new Response(previousWord); } return serveDir(req, { fsRoot: "public", urlRoot: "", showDirListing: true, enableCors: true, }); }); |
サーバーからエラーが返ってきた場合に、ブラウザでエラーをダイアログで表示するようにします。
public/index.html |
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <link rel="stylesheet" href="styles.css"> </head> <body> <h1>しりとり</h1> <p id="previousWord"></p> <input id="nextWordInput" type="text" /> <button id="nextWordSendButton">送信</button> <script type="module"> window.onload = async (event) => { const response = await fetch("/shiritori"); const previousWord = await response.text(); const para = document.querySelector("#previousWord"); para.innerText = `前の単語:${previousWord}`; }; document.querySelector("#nextWordSendButton").onclick = async (event) => { const nextWord = document.querySelector("#nextWordInput").value; const response = await fetch("/shiritori", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ nextWord }) }); if (response.status / 100 !== 2) { alert(await response.text()); return; } const previousWord = await response.text(); const para = document.querySelector("#previousWord"); para.innerText = `前の単語:${previousWord}`; }; </script> </body> </html> |
前の単語に続かない単語を入力してエラーが表示されれば OK です。
作成した Web アプリをデプロイして公開しましょう。
Deno 公式のホスティングサービスの Deno Deploy を使ってデプロイしますが、まず GitHub のリポジトリに保存する必要があります。
課題としてリポジトリを提出してもらいますので Public に設定してください。
リポジトリ名は自由に入力してもらって構いません。
リポジトリを作成したら、これまで作ったプログラムを追加しましょう。
GitHub アカウントの作成から 1週間ほどは Deno Deploy の登録ができません。
登録できない場合は、Step 13 はスキップして Step 14 に取り組んでください。
まず Deno Deploy に登録しましょう。
ページ右上の「Sign up」をクリックします。
GitHub で確認ページが表示されるので、「Authorize Deno Deploy」をクリックします。
「New Project」をクリックします。
「Deploy from GitHub repository」の項目の「Select Github repository」を選択して「Add GitHub Account」をクリックします。
GitHub で Deno Deploy アプリのインストールの確認ページが表示されるので、「All repositories」もしくは「Only select repositories」から先ほど作成したリポジトリを選択してください。
Deno Deploy のページに戻り、「Select Github repository」から作成したリポジトリを選択してください。
「Select production branch」は main を選択し、実行ファイルとして server.js を選択してください。
プロジェクト名を変更する場合は変更して「Link」をクリックします。
しばらく待つとプロジェクトが出来上がるので、「View」をクリックしてください。
デプロイされて Web アプリが公開されました!
GitHub リポジトリを更新すると自動で Deno Deploy の方も更新されて便利です。
ここまで作成した「しりとり」の Web アプリに対して、出来る限りのカスタマイズをしてください。
少なくとも以下の 5つのカスタマイズには取り組んでいただきたいです。
その他のカスタマイズの例です。
※Deno Deploy はファイルの書き込みができないので、サーバーでデータを保存する場合は外部のデータベースサービスなどと連携する必要があります
カスタマイズした Web アプリの GitHub リポジトリと、Deno Deploy の URL を提出してください。
課題の提出はこちらのフォームからお願いします。
https://forms.gle/UfYjm4Mc2Qf7ZSRf7