Skip to content
This repository has been archived by the owner on Dec 18, 2024. It is now read-only.

Latest commit

 

History

History
979 lines (710 loc) · 30.2 KB

README.md

File metadata and controls

979 lines (710 loc) · 30.2 KB

Webサービス勉強会2023 第3回

(2023/05/28) CSSとJSについて、一緒に手を動かしながら簡単におさらいしましょう!

準備

環境構築が終わっていない人は第1回資料を基に構築してください。

  1. Section03の追加を手元に取得します

WSL or Powershellを開いて(前回どちらでセットアップしたか思い出してください)

cd ~/2023-web
git pull

No Such file or directory とか言われたら以下を実行してください

  1. 2023-webリポジトリをcloneします (optional)
cd ~
git clone [email protected]:kmc-jp/2023-web.git 
  1. section03に移動します
cd ~/section03
  1. サーバーを起動します
npm install
npm run dev
  1. ブラウザからアクセスします

自動的に開くと思います

開かなければ http://localhost:8080 にアクセス

Hello World!と表示されていればOKです!

いったんCtrl+Cで止めます

HTMLの編集

  1. VSCodeの起動

~/2023-web/section03をターミナルで開いている状態で

code .

とするとVSCodeで開きます

  1. index.htmlを探す

~/2023-web/section03/index.htmlです

  1. 中身を見る
<!DOCTYPE html>
<html lang="ja-JP">
  <head>
    <meta charset="utf-8">
    <link rel="stylesheet" href="style.css">
    <script src="script.js" defer></script>
    <title>section 03 | 2023-web</title>
  </head>
  <body>
    <p>Hello World!</p>
  </body>
</html>
  1. もう一度Webサーバーを起動

ターミナルに戻って

npm run dev

http://localhost:8080 にアクセス

今日の講習会中はつけっぱにします

CSSの基礎

今回はCSSについて基礎的事項をやります。

CSSはページのデザインについて指定するための言語です。 CSSでは、特定の要素に適用するスタイルをルールとして書き並べていきます。

とにかくまずは簡単に試してみます

index.html と同じ階層に style.css があるはずです。まだ中身は空です。

以下の内容を書いてみましょう

p {
  color: red;
  font-size: 5em;
}

さて、ブラウザを開いてみましょう。見た目はどうなっていますか? 「Hello World!」の文字列がでかでかと真っ赤に表示されているでしょうか?

CSSの基本構造

p の部分をセレクターといいます。 これは適用対象を指定するものです。今回は段落要素pの全てを指定しています。 複数p要素があればすべてに適用されます。試してみましょう

中かっこ{ }の中には、プロパティと値の組み合わせを書きます。 colorfont-sizeの方がプロパティ、red5emの部分を値です。 プロパティ: 値;という形で書きます。 1つのセレクターに対してプロパティとプロパティ値の組は複数記述することができます。

index.htmlに要素を追加する

今のままだといじれる場所が少ないので、CSSの説明のために様々な要素を追加しておきます。 index.htmlを以下のように編集してください

<!DOCTYPE html>
<html lang="ja-JP">
  <head>
    <meta charset="utf-8" />
    <link rel="stylesheet" href="style.css" />
    <script src="script.js" defer></script>
    <title>section 03 | 2023-web</title>
  </head>
  <body>
    <h1>2023-web 第3回</h1>
    <p>WebService勉強会2023の<span>第3回</span>です。</p>
    <p>
      これは<a href="https://kmc.gr.jp/">KMC</a
      >が開催する春プロジェクトの一つです。
    </p>
    <div class="projects">
      <h2>春プロジェクトの一覧</h2>
      <ul>
        <li>みんなでゲームを作る2023</li>
        <li>お絵かきプロジェクト2023</li>
        <li>自宅サーバー勉強会(Linux勉強会)2023</li>
        <li>Webサービス勉強会2023</li>
        <li>サウンドプログラミング講習会</li>
        <li>Unity勉強会2023</li>
        <li>DTM練習会2023</li>
        <li>簡潔データ構造をつくろう</li>
        <li>分散システム勉強会</li>
        <li>プログラミング入門2023</li>
      </ul>     
    </div>
    <img src="./kmc.png" alt="KMC Logo">
    <div class="buttons">
      <button id="button1" type="button">これはボタンです</button>
      <button id="button2" type="button">これもボタンです</button>
      <button id="button3" type="button">これも実はボタンです</button>
    </div>
  </body>
</html>

指定方法

要素名を直接記述します。

body {
  background-color: #333333;
  color: white;
}

a {
  color: #cccccc;
  text-decoration: none;
}

全ての要素に対してCSSを割り当てる場合は、*を記述します。

全称セレクター | MDN

* {
  color: white;
}

クラス名の前に.を付けて指定します。

.projects {
  border-radius: 8px;
  box-shadow: 0 0 4px #77777777;
  display: inline-block;
  padding: 8px;
  margin: 10px;
}

id名の前に#を付けて指定します。

#button1 {
  color: white;
  background-color: #36b9ec;
  box-shadow: 3px 3px 5px #77777777;
  border-color: #00000000;
  border-radius: 8px;
  padding: 10px;
  margin: 15px;
}

[{属性名}={属性値}]の形式で指定します。属性値が存在しない場合は、[{属性名}]の形式で指定します。

a [href="https://kmc.gr.jp/"] {
  color: green;
}

基本的なプロパティ

テキスト色

テキストの色を指定するには、colorプロパティを使用します。

p {
  color: black;
}
p {
  color: #eeeeee;
}

背景色

要素の背景色を指定するには、background-colorプロパティを使用します。

body {
  background-color: gray;
}

背景画像

フォント

フォントの指定には、font-familyプロパティを使用します。

p {
  font-family: Gill Sans Extrabold, sans-serif;
}

フォントサイズの指定には、font-sizeプロパティを使用します。

.projects {
  font-size: 12px;
}
.projects {
  font-size: 1.2em;
}
.projects {
  font-size: smaller;
}

太字・斜体

太字の指定には、font-weightプロパティを使用します。

h1 {
  font-weight: bold;
}
h1 {
  font-weight: lighter;
}

斜体の指定には、font-styleプロパティを使用します。

h1 {
  font-style: italic;
}

テキスト修飾

テキスト装飾の指定には、text-decorationプロパティを使用します。

.message-important {
  text-decoration: red wavy underline;
}

文字間隔

文字間隔の指定には、letter-spacingプロパティを使用します。

.projects {
  letter-spacing: .2rem;
}
.projects {
  letter-spacing: -1px;
}

行の高さ

行の高さの指定には、line-heightプロパティを使用します。

line-heightプロパティには、単位のない値を指定するのが好ましいとされています。

.projects {
  line-height: 2;
}

枠線

枠線の指定には、borderプロパティを使用します。

.projects {
  border: 1px solid orange;
}
.projects {
  border: thick double #32a1ce;
}

角丸

角丸の指定には、border-radiusプロパティを使用します。

button {
    border-radius: 32px;
}

透明度

要素全体の指定には、opacityプロパティを使用します。

このプロパティは子要素にも継承されます。

背景色のみを透過したり、テキスト色のみを透過したりする場合は、背景色やテキスト色を透明度付きの色で指定してください。

img {
  opacity: 0.5;
}

幅・高さ

img {
  height: 250px;
  width: 500px;
}

paddingとmargin

余白を指定するプロパティに、paddingmarginがあります

paddingは、枠より内側の余白を、marginは枠より外側の余白を指定します 以下のcssを使うと分かりやすいでしょう。

.buttons {
  border: 2px solid orange;
}

#button1 {
    border: 2px solid blue;
    margin: 0;
    padding: 0;
}

#button2 {
    border: 2px solid blue;
    margin: 50px;
    padding: 0px;
}

#button3 {
    border: 2px solid blue;
    margin: 0px;
    padding: 50px;
}

CSSについてはこれくらいにしておきます。 基本的な事項さえ理解していれば後は、「やりたいこと+CSS」で検索すれば大抵のことはできるでしょう。 より詳しく学びたい人は MDNのCSSの章 を見てみましょう。

JavaScript

JavaScriptは、ページに大して動的な変化を与えるときに使われるスクリプト言語です。

まずは、index.htmlと同じディレクトリにあるscript.jsを次のように変更します。

document.addEventListener('DOMContentLoaded', () => {
  const buttons = document.querySelectorAll('button');

  for (const button of buttons) {
    button.addEventListener('click', createParagraph);
  }

  function createParagraph() {
    const p = document.createElement('p');
    p.textContent = 'ボタンが押されました!';
    document.body.appendChild(p);
  }
});

さて、ブラウザを見てください。特に見た目では何も変わっていませんね。

では、三つあるボタン、どれでもいいので押してみましょう。どうなりましたか?

「ボタンが押されました!」という文字列が下の方に表示されたのではないでしょうか。

このように、JavaScriptを使うと、「ボタンが押されたとき」などの動的なイベントによって、新しく要素を追加/削除したり、編集したりできるのです。

動作を簡単に追ってみます。

まず、script.js自体は、index.htmlの、html.head.scriptの部分で読み込まれています。ここで、defer属性をつけることで、JavaScriptの読み込みを待たずにHTMLを読み込み、DOMを作ります。HTMLは上から順番に読み込まれていくので、こうしておかないとJavaScriptが読み込まれた後の要素にアクセスできなくなってしまいます。

さて、script.jsが無事に読み込まれたので中身の処理を見ていきます。 まず、

document.addEventListener('DOMContentLoaded', () => {
  ...
});

とありますね。documentはブラウザで表示されているウェブページを表しています。Webページのコンテンツは、「DOM(Document Object Model)」として表現されます。

そして、addEventListenerはターゲットで特定のイベントが起こった時に呼び出される関数を設定します。今回は、DOMContentLoaded、つまりページが読み込まれた時に、第二引数のコールバック関数が実行されます。

コールバック関数の中身は

const buttons = document.querySelectorAll('button');

for (const button of buttons) {
  button.addEventListener('click', createParagraph);
}

function createParagraph() {
  const p = document.createElement('p');
  p.textContent = 'ボタンが押されました!';
  document.body.appendChild(p);
}

のようになっています。

まず、

const buttons = document.querySelectorAll('button');

の部分では、querySelectorAllを使って、条件に合う要素一覧を取得しています。条件の部分はCSSでのセレクタと同じように指定してください。

そしてその一覧をbuttonsという名前の変数に保存します。

次に

for (const button of buttons) {
  button.addEventListener('click', createParagraph);
}

forを使って、buttonsの中身一つずつに処理を実行します。 その処理内容は、addEventListenerを使い、ボタンに対してclickイベントが起こった時にcreateParagraph関数を実行してね、と設定することです。

createParagraph関数はその次で定義されています。

function createParagraph() {
  const p = document.createElement('p');
  p.textContent = 'ボタンが押されました!';
  document.body.appendChild(p);
}

まず、document.createElement('p')で、<p>要素を作り、pという変数に入れます。 そして次に、ptextContentというプロパティに「ボタンが押されました!」という文字列を設定します。つまりこれは<p>ボタンが押されました!</p>と同じです。

最後に、document<body>に対して、p要素を追加します。

よって、みなさんに見てもらったように、ボタンを押すと「ボタンが押されました!」がどんどん追加されるような挙動になったのです。

さて、簡単にJavaScriptとその動きが分かったところで、MDNの入門講座の題材にチャレンジしてみましょう!

今回は数あてゲームを作ります。ランダムに生成した1~100までの数字を10回以内に当てていくゲームです。

さて、まずはHTMLを用意します

index.htmlを次のように書き換えましょう。

<!DOCTYPE html>
<html lang="ja-JP">
  <head>
    <meta charset="utf-8" />
    <link rel="stylesheet" href="style.css" />
    <script src="script.js" defer></script>
    <title>section 03 | 2023-web</title>
  </head>
  <body>
    <h1>数字当てゲーム</h1>
    <p>1 から 100 までの数字を当ててみて!10 回以内に当てられるでしょうか。選んだ数字が大きいか小さいかを表示します。</p>
    <div class="form">
      <label for="guessField">予想を入力してください: </label>
      <input type="text" id="guessField" class="guessField" />
      <input type="submit" value="予想を送信" class="guessSubmit" />
    </div>
    <div class="resultParas">
      <p class="guesses"></p>
      <p class="lastResult"></p>
      <p class="lowOrHi"></p>
    </div>
  </body>
</html>

タイトルのh1、ゲームの説明用のp、答えを投稿するフォームとそのテキスト、入力ボックス、送信ボタンを追加しました。

その下のresultParasは結果の表示に使います。今のところその中身は空なのでブラウザでは真っ白です。

さて、次にscript.jsを書き換えます。

まずは今までの物を消して、以下の通りにします

document.addEventListener("DOMContentLoaded", () => {
    let randomNumber = Math.floor(Math.random() * 100) + 1;

    const guesses = document.querySelector('.guesses');
    const lastResult = document.querySelector('.lastResult');
    const lowOrHi = document.querySelector('.lowOrHi');

    const guessSubmit = document.querySelector('.guessSubmit');
    const guessField = document.querySelector('.guessField');

    let guessCount = 1;
    let resetButton;

    guessField.focus();
});

色々書きましたね。addEventListenerはさっきと同じなので置いておいて、そのコールバック関数の中身を見ていきます。

まず最初に答えとなる乱数を設定します。Math.random()は0以上1未満の乱数を生成するので、百倍して、整数部分を取り出すことで0~99の乱数を取り出し、+1して1~100までの乱数が取り出せますね。結果はrandomNumberに格納しておきます。

次にquerySelector<p class="guesses"></p> <p class="lastResult"></p> <p class="lowOrHi"></p> の三つを探し出して、それぞれ変数に代入します。この要素の子要素を書き換えることでメッセージなどを表示させていきます。

同じようにして、入力ボックスのguessField、送信ボタンのguessSubmitも変数に代入します。

そして、guessCountという、予想回数を入れておく変数を作り、1を入れておきます。

resetButtonの変数も最初に作っておきます。中身はまだ空です。

さて、guessField.focus()を入れてあげることで、ページが読み込まれたとき、自動的にカーソルがguessField、つまり入力欄に飛ぶようにしておきます。こうしておくと最初に入力ボックスをマウスでクリックする必要はなく、すぐに数字を入力できます。このような細かい親切設計は重要です。

さて、次にcheckGuess()関数を作ります。これは、guessSubmit.addEventListener('click', checkGuess);として、送信ボタンを押したときに実行される関数です。

guessField.focus();の下に以下を追加します。

function checkGuess() {
  let userGuess = Number(guessField.value);
  if (guessCount === 1) {
    guesses.textContent = '前回の予想: ';
  }
  guesses.textContent += userGuess + ' ';

  if (userGuess === randomNumber) {
    lastResult.textContent = 'おめでとう! 正解です!';
    lastResult.style.backgroundColor = 'green';
    lowOrHi.textContent = '';
    setGameOver();
  } else if (guessCount === 10) {
    lastResult.textContent = '!!!ゲームオーバー!!!';
    setGameOver();
  } else {
    lastResult.textContent = '間違いです!';
    lastResult.style.backgroundColor = 'red';
    if(userGuess < randomNumber) {
      lowOrHi.textContent='今の予想は小さすぎです!' ;
    } else if(userGuess > randomNumber) {
      lowOrHi.textContent = '今の予想は大きすぎです!';
    }
  }

  guessCount++;
  guessField.value = '';
  guessField.focus();
}

guessSubmit.addEventListener('click', checkGuess);

長いですが一つずつ見ていきましょう。

checkGuess()の中ではまず、userGuessを定義し、その中身はNumber(guessField.value)となっています。つまり、「guessField=入力ボックス」の中身のテキストを数字に変換して代入しているのです。

つぎに

if (guessCount === 1) {
  guesses.textContent = '前回の予想: ';
}

では、guessCount === 1、つまり、初回の入力の時にだけ、guesses=<p class="guesses">に対して、「前回の予想: 」というテキストを追加しています。

次に、guesses.textContent += userGuess + ' '; の部分で、もともとのguesses.textContentの中身にuserGuessつまり、ユーザーから受け付けた数字を追記します。こうすると前回予想した数字が羅列されるのでわかりやすいですね。

次は、クソ長条件分岐ですね。

if (userGuess === randomNumber) {
  lastResult.textContent = 'おめでとう! 正解です!';
  lastResult.style.backgroundColor = 'green';
  lowOrHi.textContent = '';
  setGameOver();
} else if (guessCount === 10) {
  lastResult.textContent = '!!!ゲームオーバー!!!';
  setGameOver();
} else {
  lastResult.textContent = '間違いです!';
  lastResult.style.backgroundColor = 'red';
  if(userGuess < randomNumber) {
    lowOrHi.textContent='今の予想は小さすぎです!';
  } else if(userGuess > randomNumber) {
    lowOrHi.textContent = '今の予想は大きすぎです!';
  }
}

まず、(userGuess === randomNumber)の部分で、入力された数字と答えの数字が一致しているかどうかをみます。ここで等しければ、 lastResult.textContent、つまり<p class="lastResult"></p> のテキスト部分を、「おめでとう! 正解です!」に変更します。

そして、lastResult.style.backgroundColor = 'green';の部分で背景の色を緑色にします。 最後に、lowOrHi.textContent = '';によって、結果表示部分のテキストを空にしてあげます。

setGameOver()は、ゲーム終了後のふるまいを定義した関数です。今のところまだ存在しないですが放置します。

さて、正解ではなかった時、(guessCount === 10)を行い、試行回数が10回になったかどうかを判定します。今回は、予想は10回までという仕様だったので、guessCountが10になった時ゲームオーバーにします。

中身は、lastResult.textContent = '!!!ゲームオーバー!!!';によってゲームオーバーだよ~と伝え、setGameOver()を実行して、ゲームを終了させます。

最後にどちらに当てはまらなかった場合、つまり不正解だった場合、lastResult.style.backgroundColor = 'red';で背景を赤色にします。

そして間違いの中にも大きすぎと小さすぎがありますね。if(userGuess < randomNumber)で判定します。

入力が大きすぎたときはlowOrHi.textContent='今の予想は小さすぎです!'によって、結果を表示してあげます。

逆に小さかったときはlowOrHi.textContent = '今の予想は大きすぎです!';とします。

条件分岐を抜けたら最後に

guessCount++;
guessField.value = '';
guessField.focus();

を追加します。

guessCount++guessCount = guessCount + 1と同値です。送信ボタンが押されるたびに、実行回数のカウントを進めていくのですね。

そして、次の試行の際に入力ボックスに前回の数字が残っていると邪魔ですね。なのでguessField.value=''で、中身を空にします。

そして最後にguessField.focus()を実行して入力ボックスにフォーカスを与えてあげます。

ここまでがcheckGuess()関数でした。

最後に、この関数をaddEventListenerを使って、提出ボタンに付けてあげます! guessSubmit.addEventListener('click', checkGuess);の部分ですね。

さぁ!これで正誤判定まで完成しました!一度遊んでみましょう!!setGameOver()がまだ定義されていないのでエラーが出ますが無視しましょう!

さて、今の状態では一度しか遊べませんね。繰り返し遊べるように変更しましょう。そのためにsetGameOver()を作ります。

以下コードをguessSubmit.addEventListener('click', checkGuess);の下に追加しましょう。

function setGameOver() {
  guessField.disabled = true;
  guessSubmit.disabled = true;
  resetButton = document.createElement('button');
  resetButton.textContent = '新しいゲームを始める';
  document.body.appendChild(resetButton);
  resetButton.addEventListener('click', resetGame);
}

中身を見ていきます。

まずは、入力ボックスと提出ボックスを無効化します! ゲーム終了後にも予想できたら意味がないですね。

guessField.disabled = true;
guessSubmit.disabled = true;

とすれば無効化されます。

次にゲームのリセットボタンを追加します。 resetButton自体の定義は上の方でしました。

resetButton = document.createElement('button');

で新しいボタン要素を作成します。

resetButton.textContent = '新しいゲームを始める';

そしてボタンのテキストを「新しいゲームを始める」にします。

作ったボタンを

document.body.appendChild(resetButton);

bodyに追加してあげます。

最後に

resetButton.addEventListener('click', resetGame);

で、作ったボタンに対して、addEventListenerでクリック時に実行するコールバック関数を指定してあげます。

ここまでがsetGameOver()関数です。

さて、リセットボタンを押したときに実行されるresetGame()も作る必要が出てきました。作りましょう!

function setGameOver() {} の下に

function resetGame() {
  guessCount = 1;

  const resetParas = document.querySelectorAll('.resultParas p');
  for (let i = 0 ; i < resetParas.length ; i++) {
    resetParas[i].textContent = '';
  }

  resetButton.parentNode.removeChild(resetButton);

  guessField.disabled = false;
  guessSubmit.disabled = false;
  guessField.value = '';
  guessField.focus();

  lastResult.style.backgroundColor = 'white';

  randomNumber = Math.floor(Math.random() * 100) + 1;
}

を追記します。

まず、guessCount = 1;で試行回数を1にリセットします。 次に、const resetParas = document.querySelectorAll('.resultParas p');で、<div class="resultParas">の中の子要素<p>を検索して、その一覧をresetParasに入れてあげます。

言い忘れていますが、querySelectorAllのセレクタ部分は、スペース区切りで複数設定できます。今回はclass=resultParasかつ、<p>要素というものにヒットします。classについては親要素に設定されているので、その子要素にも適用されます。

  for (let i = 0 ; i < resetParas.length ; i++) {
    resetParas[i].textContent = '';
  }

の部分では、取得してきた<p>要素たちに対して、全てのテキストを空にしています。前回の結果のテキストなどが残っていてはいけませんからね。

resetButton.parentNode.removeChild(resetButton);

の部分は、resetButtonを消すためのコードです。

resetButton.parentNodeで、resetButtonの親要素、つまり<body>を指定します。そしてそこからremoveChild(resetButton)で、resetButtonを削除しています。

guessField.disabled = false;
guessSubmit.disabled = false;

では先ほど無効化した入力ボックスと送信ボタンを再度有効化します。disabledfalseなのでenabledですね。

あともう少しです。

guessField.value = '';
guessField.focus();

で、入力ボックスの中身を空にします。 そして、最後にまたまた入力ボックスにフォーカスさせます。

あ、背景の色を変更したのを忘れていましたね。元の色に戻してあげましょう。

lastResult.style.backgroundColor = 'white';

これで白色に戻りました。

いよいよ最後です!乱数をもう一度生成します!

一番最初とほとんど同じですが、letを付けずに、既存のrandomNumberを上書きするようにします。

randomNumber = Math.floor(Math.random() * 100) + 1;

これで完成です!!!! 何回でも無限に遊べますね!

完成したscript.js全体を以下にのせておきます!

document.addEventListener("DOMContentLoaded", () => {
    let randomNumber = Math.floor(Math.random() * 100) + 1;

    const guesses = document.querySelector('.guesses');
    const lastResult = document.querySelector('.lastResult');
    const lowOrHi = document.querySelector('.lowOrHi');

    const guessSubmit = document.querySelector('.guessSubmit');
    const guessField = document.querySelector('.guessField');

    let guessCount = 1;
    let resetButton;

    guessField.focus();

    function checkGuess() {
        let userGuess = Number(guessField.value);
        if (guessCount === 1) {
          guesses.textContent = '前回の予想: ';
        }
        guesses.textContent += userGuess + ' ';
      
        if (userGuess === randomNumber) {
          lastResult.textContent = 'おめでとう! 正解です!';
          lastResult.style.backgroundColor = 'green';
          lowOrHi.textContent = '';
          setGameOver();
        } else if (guessCount === 10) {
          lastResult.textContent = '!!!ゲームオーバー!!!';
          setGameOver();
        } else {
          lastResult.textContent = '間違いです!';
          lastResult.style.backgroundColor = 'red';
          if(userGuess < randomNumber) {
            lowOrHi.textContent='今の予想は小さすぎです!' ;
          } else if(userGuess > randomNumber) {
            lowOrHi.textContent = '今の予想は大きすぎです!';
          }
        }
      
        guessCount++;
        guessField.value = '';
        guessField.focus();
      }

      guessSubmit.addEventListener('click', checkGuess);

      function setGameOver() {
        guessField.disabled = true;
        guessSubmit.disabled = true;
        resetButton = document.createElement('button');
        resetButton.textContent = '新しいゲームを始める';
        document.body.appendChild(resetButton);
        resetButton.addEventListener('click', resetGame);
      }

      function resetGame() {
        guessCount = 1;
      
        const resetParas = document.querySelectorAll('.resultParas p');
        for (let i = 0 ; i < resetParas.length ; i++) {
          resetParas[i].textContent = '';
        }
      
        resetButton.parentNode.removeChild(resetButton);
      
        guessField.disabled = false;
        guessSubmit.disabled = false;
        guessField.value = '';
        guessField.focus();
      
        lastResult.style.backgroundColor = 'white';
      
        randomNumber = Math.floor(Math.random() * 100) + 1;
      }
});

後片付け

ターミナルでCtrl+CYを入力してサーバーを終了させます。