Web Developer基礎固めの部屋
改めてWEB Developerとしての基礎を固める
・JavaScriptを自分でコントロールしている感を得るために基礎を固める
・そもそものWEBの基礎から体系的に学んで知識を定着する
・エンジニアとしてのマインドセット、考え方、解決方法を学ぶ
Udemy講座【世界で70万人が受講】Web Developer Bootcamp 2023(日本語版)を受講
・演習問題は全て解く
・真似するだけで終わらない、理解してから進む
・時間をかけてOK、理解することの方が大事。
・理解するために些細なことでもZennのスクラップに投稿していく
day1 10/21
インターネットとは
様々な情報を運ぶためのインフラ
メール、WEB、ファイル、ゲームなど...etc
データ転送をするのはルーティングを行なっている。
様々なデバイスが繋がっている。
WEBとは
ワールド・ワイド・ウェブ
インターネットを介して、文書などのリソースを提供する情報システム
→これはURLが割り当てられる。ページもリソースと呼べる。
やり取りはHTTP(HTTPS)でやり取りされる。これが基盤になる。
→プロトコル
サーバー(WEBサーバー)とは
リクエストを受け取ってレスポンスを返す。WEBでリクエストを受け付けることのできる機器
これらがHTTPによって成り立っている。
クライアントとは
リクエストする側はクライアント、自分自身、自分のパソコン
クライアントサイド、サーバーサイドで分かれている。
クライアントサイドからリクエストを送ると、サーバーは部品と説明書を返す。
それをブラウザで組み立てる。
例)ikeaで部品と説明書を買って、家で組み立てるようなもの!完成した画面を返すわけじゃない!
HTML,CSS,JavaScriptをブラウザに返す。それをブラウザが解釈して見やすいように画面を組み立てている。
フロントエンドとバックエンドの違い
フロントエンド
ブラウザからリクエストを飛ばすのはフロントエンド
レストランで言うとウェイター
言語:HTML,CSS,JavaScript
フロントの表示に集中する
バックエンド
リクエストを受け取ったサーバーが調理してフロントに送り返す
レストランで言うとキッチン
言語:Java,Python,C++,etc...
処理に集中する
HTML,CSS,JavaScript
HTML 名詞 骨組み・物 例)恐竜
CSS 形容詞 見た目・装飾 例)紫色の
JavaScript 動詞 動き・機能 例)踊っている
day2 10/22
Formについて
actionを指定したところに対して、httpリクエストを送ることができる。
例)search,
<form action=""></form>
action属性:【どこに】フォームの情報を送信するかを指定する。
method属性:フォームを送信する際に、ブラウザーが使用するHTTPメソッドを指定する
httpでフォームactionで送信するとURLにパスワードとかもURLに反映してしまう。これは非常によろしくない。そんな時はpostを使用する。
【応用と遊び】自分のサイトからYoutube検索をしてみる
改めてform,actionの仕組みについて理解できた!
今までなんとなくだったから裏側の仕組みを知って応用して動くと楽しい。
<!-- どこで検索するか -->
<form action="https://www.youtube.com/results">
<!-- https://www.youtube.com/results?search_query=検索結果 -->
<input type="text" name="search_query" placeholder="検索名" />
<button type="submit">Youtubeで検索</button>
</form>
Youtubeはsearch_query=で検索されているので、name属性を合わせて、formの送信の向き先をYoutubeの検索結果URLから拝借して合わせれば、自作サイトからYoutube検索ができる。
バリデーションはフロントエンドとサーバーサイド両方でかける
なぜか?
サーバーサイドでバリデーションを設定しないとターミナルからhttpリクエストを飛ばした際に、メールのところにメールじゃないものが入ったり、文字数が足りないままリクエスト飛んじゃったりする対策として
なぜフロントエンドにもバリデーションをつけるのか?
単純にユーザーの見やすさの問題。UI的に
day3 10/26
学習2日空いてしまった、実務の忙しいタイミングとかもちろん波はあるので、夜の仕事終わりで時間決めて勉強していく!
CSSパート
知らなかったことまとめ
普段使う機会少なそうだが知っておくと便利そう。
/* 1文字目だけスタイルを当てる */
h2:first-letter {
font-size: 50px;
}
/* 1行目だけ指定してスタイルを当てる */
p::first-line {
color: purple;
}
day4 10/28
JavaScript編突入
今の1番の課題、コントロールしている感を得るためにも時間をかけて理解する。
データ種類
プリミティブ型・・・JavaScriptに最初から装備されている型
Number
String
Boolean
Null
Undefined
Number
JavaScriptには数値型は一つしかない
正・負、整数・不動小点数
%剰余演算 偶数なのか奇数なのかの演算で使用
** 冪演算
9 % 2
1
3 ** 3
= 27
3の3乗の計算
NaN
not a number
数字じゃない、numberの仲間
NaNとの演算は常にNaNになる。
0/0
= NaN
NaN / NaN
= Nan
200 + 0/0
= NaN
いつ使うのか?
フォームなどでユーザーが入力された数値、受け取った数値がちゃんと数字かの判定、不正入力数値の検出。
外部APIなどから受け取った数値が数字かの判定で使用される。
変数
値に名前をつけて管理するラベルのようなもの、後で参照できる。あとから違う値に変えることもできる。
let year = 1985;
let hoge = 5;
let hogehoge = 1;
let total = hoge + hogehoge;
<!-- 足し算 -->
let score = 0;
score +=1;
= 1
score++;
2
<!-- 引き算 -->
score--;
1
変数宣言
const・・・定数、上書き不可、変更しない値に使用する。
var・・・昔はこれしかなかったが今はあんまり使わない。
let・・・上書き可能、値を変更する必要がある場合に使用。
Boolean
trueかfalseかの判定
変数の中に格納したデータ型はあとから変えられる。けど、、、あんまり使う機会ない?
TypeScriptの場合はデータ型に制限があるが、バニラJavaScriptの場合はそれがないので注意。
変数の命名規則
絶対遵守規則
<!-- NG -->
<!-- 空白が入るのはNG -->
let hello world
<!-- 数字で始めるのはNG -->
let 123hello
開発者のための規則
キャメルケースにして書く、一般的に多く使用されている。
わかりやすい変数名にする。別の人が見てわからない変数名は使用しない。
<!-- 単語の先頭文字を大文字にする -->
let currentYear
<!-- NG -->
<!-- 何の数字? -->
let y = 1995;
<!-- Booleanの変数名の先頭にはis,hasをつける。 -->
let isLogginedInUser
day4 10/28 part2
String型
文字の並びを表すデータ型テキストを表現する、"",''で囲む。
※""と''を混同しない、どちらかに必ず統一する!
let userName = "山田";
String型はindexされている
const cat = "string cat";
cat[0]
<!-- 結果はs
変数catの0文字目 -->
<!-- 文字数を数える -->
cat.length
toUpperCase
let hello = 'hello world';
const upperHello = hello.toUpperCase();
upperHello;
<!-- 結果 -->
HELLO WORLD
<!-- メソッドは組み合わせられる -->
let greeting = ' hello world ';
const trimGreeting = greeting.trim().toUpperCase();
<!-- 結果 -->
'HELLO WORLD'
メソッドを連結することをメソッドチェインという。よく使用する。
replace正規表現、文字列を置き換えることができる。
const hello = "hello world";
hello.replace("hello", "goodnight")
<!-- 結果 -->
'goodnight world'
テンプレートリテラル
バックチックで囲むと文字列と変数・式等を同時に使用できる。
let product = 'チョコレート'
let price = 100;
let quantity = 4;
`${product}を${quantity}個買って${price}円だった。`
NullとUndefined
Undefinedはわからない。定義されていない状態。
Nullは意図的に値がない。無いことを意図的に宣言する。
<!-- ユーザーログイン前 -->
let loggedInUser = null;
<!-- ユーザーがログインした -->
loggedInUser = 'tom';
day5 11/24
基礎的な部分だが等価についての再理解
比較演算子
// 比較演算子
const math = 10 === 10;
// console.log(math);
// == 等価
// 値が等しいかチェックするが、型が等しいかはチェックしない
const math2 = null == undefined;
// console.log(math2); // trueになる
const math3 = 1 !== 1;
// console.log(math3);
// === 厳密な等価
// 比較の際は基本===を使用する。
論理演算子
and
index.OF("7") !== -1 は、文字列の中に7が存在する場合trueになる式
7が存在すれば文字列の何番目かを返す=true
7が存在しなければ-1を返す
今回は-1ではないの判定なので-1でなければtrueになる。
const mystery = 'Prompt7';
if(mystery[0] === 'P' && mystery.length > 5 && mystery.indexOf('7') !== -1){
console.log("正解!");
}
or
どちらかがtrueならtrueになる式
// or ||
// 片方の式がtrueならtrueと言える
const age = 18;
// 0歳以上5歳未満 or 65歳以上
if ((age >= 0 && age < 5) || age >= 65) {
console.log("無料");
} else if (age >= 5 && age < 10) {
console.log("子供料金1000円");
} else if (age >= 10 && age < 65) {
console.log("大人料金5000円");
} else {
console.log("無効な年齢です");
}
NOT演算子
if文とswitch文どちらがいいかはプロジェクトによりけり。
明らかにif else.......でネスト深かったり多い場合はswicth文が有効。
// NOT演算子 !
// trueとfalseの値を判定させる
// falsyな場合、trueに変換、truthな場合、falsyに変換
let userName = prompt("ユーザー名を入力してね");
// userNameが"""空文字でfalsyなので!で反転してtruthyになってif文の中身が実行される
if (!userName) {
userName = prompt("問題が起きました。ユーザー名を入力してね。");
}
SWITCH文
// SWITCH文
// ケースが実行されるとその下まで実行されるbreakで逃げないとずっと実行される
// switch文の特定を活かして、6,7が一緒の出力をしたいならbreakを入れずに7にスキップさせることもできる
const day = 7;
switch (day) {
case 1:
console.log("月曜日");
break;
case 2:
console.log("火曜日");
break;
case 3:
console.log("水曜日");
break;
case 4:
console.log("木曜日");
break;
case 5:
console.log("金曜日");
break;
case 6:
case 7:
console.log("週末");
break;
default:
console.log("無効な数字です");
}
day6 11/26
配列
配列とは、順序を持った値のコレクション
push - 末尾に追加
pop - 末尾を取り除く
shift - 先頭を取り除く
unshift - 先頭に追加
// 配列の基本
let students = ["smith", "paul", "john", "teresa"];
console.log(students);
students[0] = "dave";
// pushメソッドを使用して配列の最後に要素を追加する
students.push("ringo", "ando");
console.log(students);
// popメソッドを使用して配列の最後の要素を取り除く
const person = students.pop();
console.log(person);
console.log(students);
// shiftメソッドを使用して配列の先頭の要素を取り除く
const person1 = students.shift();
console.log(person1);
// unshiftメソッドを使用して配列の先頭に要素を追加する
students.unshift("VIP");
console.log(students);
// include()あるか?
console.log(students.includes("smith"));
// indexOf()何番目にあるか・
console.log(students.indexOf("smith"));
// reverse()要素が逆順になる 注意:元々の配列が変更される。不変性が破壊される、本当は新しい配列を作った方がいい。
students.reverse();
console.log(students);
// slice()メソッド 不変性は保たれる、別の配列として切り取る、あんまり使わない
console.log(beatles.slice(1));
// splice()メソッド 元の配列を変更する、不変性が破壊される。splice(何番目から, 何個消すか)
console.log(beatles.splice(0, 2));
console.log(beatles);
// splice(何番目から, 何個消すか, 何を追加するか)
console.log(students.splice(1, 0, "secondary"));
console.log(students);
// sort()メソッド 並び替えができる、本質は関数の中で自分で並び順を決めて使う
let scores = [1, 49, 89, 100, 90, 0];
console.log(scores.sort());
配列の等価性
これはReactでも大事な要素なので重要。
中身じゃなくて、同じ配列を参照しているかどうかが大事。
// 配列の等価性は中身じゃなくて、全く同じ配列を参照しているかどうかを判定している
let numbers = [1, 2, 3, 4, 5];
let numbersCopy = numbers;
console.log(numbers === numbersCopy); // true 同じ配列
配列のネスト
今後めちゃくちゃ使う、オブジェクトも。
アクセスの仕方も冷静になって考えればシンプル。
// 配列のネスト
const board = [
["0", null, "x"],
["0", null, "これだよ!"],
["0", null, "x"],
["0", null, "x"],
["0", null, "x"],
["0", null, "x"],
];
// 配列の1番目と2番目にアクセス
console.log(board[1][2]); // これだよ!
day7 11/27
オブジェクト
オブジェクトとは、プロパティの集合体キーと値のペア、
キーを使用してオブジェクトにアクセスできる。順序はない
// オブジェクト
// プロパティの集合体、キーと値のペア、インデックスではなく、キーを使ってアクセスする。
// データにラベリングをできる、順序はない
// オブジェクトリテラル {}で囲んでkeyとvalueのペアのこと
const person = {
firstName: "taro",
lastName: "yamada",
};
// オブジェクトのキーはStringに変換される
console.log(person.firstName); // personのkey:firstNameにアクセスする
console.log(person["firstName"]);
const restaurant = {
name: "Ichiran Ramen",
address: `${Math.floor(Math.random() * 100) + 1} Johnson Ave`,
city: "Brooklyn",
state: "NY",
zipcode: "11206",
};
const fullAddress = `${restaurant.address}${restaurant.city}${restaurant.state}${restaurant.zipcode}`;
console.log(fullAddress);
ループ処理
処理を繰り返すためにある。
- 10回繰り返す
- 配列の中の全数字の和を求める
ループの種類
- for
- while
- for...of
- for...in
forループ
// forループ
// for(初期値; 条件式; 増減式 )
// 初期値iを1として、iが10以下の間はiに+1処理を繰り返す
for (let i = 1; i <= 10; i++) {
console.log(`出力結果は${i}です。`);
}
forループのネスト
配列の中に配列が入っているときなどその中身を取り出してループさせたい時に有効。
// ループのネスト
for (let i = 0; i <= 10; i++) {
console.log(`iは${i}`);
for (let j = 1; j < 4; j++) {
console.log(` jは${j}`);
}
}
const student = [
["伊藤", "田中", "前田"],
["明智", "豊富", "卑弥呼"],
["伊能", "中大兄皇子", "綱吉"],
];
for (let i = 0; i < student.length; i++) {
// まず配列を変数に格納する
const row = student[i];
console.log(`${i + 1}行目`);
// 変数に格納した配列をさらにrowの長さ分ループさせる
for (let j = 0; j < row.length; j++) {
console.log(row[j]);
}
}
whileループ
終了のタイミングがわからない、ループ回数が不明な場合のループに有効。
ユーザーのパスワード認証とか、ゲームの勝敗がつくまで続けるとか。
// whileループ
// 更新式を書いていないと簡単に無限ループに陥るので注意
let num = 0;
while (num < 10) {
console.log(num);
num++;
}
// パスワード一致するまでループ
const SECRET = "secret";
let guest = ""; // 初期値は空文字
while (SECRET !== guest) {
guest = prompt("パスワードを入力してください。");
}
guest = alert("認証成功");
breakでループを抜ける
parseInt部分とか理解浅かったので理解するためのいい機会になった。
あくまで受け取った変数の値は変えずにwhileで比較するためにparaseIntで整数変換した上でtagetNumと比較をする。
whileでもforでも、breakを使用すると、その条件に当てはまるときに処理を抜けることができる。
// breakでループを抜けることができる
let input = prompt("何か入力");
while (true) {
input = prompt(input);
// 入力された値がquitなら終了
if (input === "quit") {
break;
}
}
while文を使用したクイズゲーム
// クイズゲーム
let maximum = parseInt(prompt("好きな数字を入力してね。"));
// 数字が入るまで繰り返し入力を求める
while (!maximum) {
maximum = parseInt(prompt("エラーです。有効な数字を入力してください。"));
}
const targetNum = Math.floor(Math.random() * maximum) + 1;
console.log(targetNum);
let guess = parseInt(prompt("1つ数字を決めました。数字を当ててみてね。"));
let count = 1;
// guessで受け取った文字列を整数に変換してランダムに生成されるtargetNumと比較
// 数値で比較するために必要、これがないと永遠に終わらない
// parseInt(guess)では、受け取ったguessを整数にした上で比較をするので、guessの値自体を変えるものではない。
while (parseInt(guess) !== targetNum) {
// ユーザーがqを入力するとループを抜ける
// guessは文字列の入力も受け取る(ここではparaIntしてないのでこれは成り立つ)
if (guess === "q") break;
count++;
if (guess > targetNum) {
guess = prompt("その値よりは小さいです!");
} else {
guess = prompt("その値よりは大きいです!");
}
}
if (guess === "q") {
alert("終了します。");
} else {
alert(`正解!🎉おめでとう!!${count}回で正解しました!`);
}
for of
for文よりもシンプルに配列の中身を取り出して使用することができる。
// 対象の配列と一個一個の名前を用意するだけで全て取り出せる
for (let subreddit of subreddits) {
console.log(subreddit);
}
// stringも列挙可能なオブジェクトのため1文字づつ取り出せる
for (let char of "helloWorld") {
console.log(char);
}
簡単なtodoアプリ
while文とif文を使用した簡単なtodoアプリ。
配列から要素を削除する際のsplice()メソッドの部分で少しつまづいた、
splice()の戻り値は配列なので、戻ってきた配列(切り取った配列)の中身にアクセスする必要がある。
let input = prompt("コマンドを入力してください。(new,list,delete,quit)");
const todos = ["水やりをする", "種まきをする"];
while (input !== "quit" && input !== "q") {
// list
if (input === "list") {
console.log("-----------");
for (let i = 0; i < todos.length; i++) {
console.log(`${i}:${todos[i]}`);
}
console.log("-----------");
// new
} else if (input === "new") {
const newTodo = prompt("新しいtodoを入力してください。");
todos.push(newTodo);
console.log(`${newTodo}を追加しました。`);
// delete
} else if (input === "delete") {
const index = parseInt(prompt("削除したいindexを入力"));
// indexに入力された値がNaNでないかの判定
if (!Number.isNaN(index)) {
const deleted = todos.splice(index, 1); // 入力された数値の位置から1個削除 splice(削除する位置, 何個削除するか)
// splice()は必ず配列として返すから、その中の唯一の中身を取り出すためにdeleted[0]としている。
console.log(`${deleted[0]}を削除しました。`);
} else {
console.log("有効なindexを入力してください。");
}
}
input = prompt("コマンドを入力してください。(new,list,delete,quit)");
}
console.log("アプリを終了します。");
関数
- 関数とは、再利用可能なコードをまとめたもの。
- まとまったコードを定義して、あとで実行できる
- いろんな場面で使用可能
定義して、実行する。
基本的な書き方
// 関数(function)
// JavaScriptのホイストで変数や関数の宣言をコードの先頭に移動させるので定義した関数の上で呼び出しても実行される
// ただ基本的には関数を上で定義して、下で実行するのが良さげ
singSong();
// 関数を定義
function singSong() {
console.log("あああああ");
console.log("Whoooooooooooooo");
console.log("Uhooooooooooo");
}
// 定義した関数を実行
singSong();
引数
引数(ARGUMENTS)喧嘩の意味らしい。
関数の入力値のようなもので、引数を渡して実行することで関数内の値を変更することができる。
// 引数
// 関数の入力値のようなもの
// 関数は機械のようなもの、実行すれば動く
// greet(パラメーター)
function greet(firstName, lastName) {
console.log(`Hello! ${firstName} ${lastName}`);
}
// // 引数
greet("Tommy", "muscle");
// 引数を渡さないと、そこにはundefinedが返ってくる
greet("Tommy");
greet();
複数の引数
引数は1つだけじゃなく複数の値を渡すことができる。
// 複数の引数
function greet(firstName, lastName) {
console.log(`Hello! ${firstName} ${lastName[0]}さん`);
}
greet("MukiMuki", "muscle");
// strで受け取った値を第二引数の数分連結させて出力する関数
function repeat(str, count) {
let result = "";
for (let i = 0; i < count; i++) {
result += str;
}
console.log(result);
}
// hello!!の文字列を3回結合
repeat("hello!!", 3);
returnの性質
- returnが実行されるとそこで処理は終了する
- returnが返せる値は一つのみ
// returnを使うと関数から値を返すことができる、returnした時点でその関数の実行が終わる。
function add(x, y) {
if (typeof x !== "number" || typeof y !== "number") {
return false;
}
// if文の中身がtrueになって中の処理が実行されると、その時点でreturn false が返されて処理は終わり
// そうじゃないならif文を通り越して下の処理が実行されて終わり。つまりreturnが返せる値は一つのみ。
// returnが実行されると処理は終わり
return x + y;
}
let total = add(5, 8);
console.log(total);
スコープ
関数の中で定義した変数は基本関数の外では参照できない。
let deadlyAnimal = "ヒョウモンダコ";
function handleAnimal() {
let deadlyAnimal = "カサゴ";
console.log(deadlyAnimal);
}
handleAnimal(); // カサゴ
console.log(deadlyAnimal); // ヒョウモンダコ
// 関数の外では影響がないのでヒョウモンダコが出力される
// レキシカルスコープはコードのどこで定義したかで決まる、実行時ではない
let x = "ああああ";
function hoge() {
console.log(x);
}
function moge() {
let x = "かきくけこ";
hoge();
}
hoge(); // ああああ
moge(); // ああああ
関数式
// 関数式
// 関数を変数に格納できる。呼び出す時も変数として呼び出せる。
const add = function (x, y) {
return x + y;
};
console.log(add(3, 6));
const square = function (num) {
return num * num;
};
console.log(square(8));
高階関数
関数を引数として受け取る。戻り値に関数を指定する。
めちゃくちゃよく使う処理。
// 高階関数、関数とやり取りをする関数。
// 引数として関数を受け取る、戻り値に関数を指定する
function callTwice(func) {
func();
func();
}
function callTenTimes(f) {
for (let i = 0; i < 10; i++) {
f();
}
}
function rollDie() {
const roll = Math.floor(Math.random() * 6) + 1;
console.log(roll);
}
// 実行しない!!!渡すだけ!
callTwice(rollDie);
callTenTimes(rollDie);
day8 11/29
メソッド
オブジェクトの中にキーとして関数を定義することができる。
※thisが参照するのは、必ずしもそのオブジェクト内であるとは限らない。
// メソッド:オブジェクトのキーに関数を定義できる。
const myMath = {
PI: 3.14,
square: function (num) {
return num * num;
},
cube: function (num) {
return num * num * num;
},
};
console.log(myMath.square);
// 省略形
const myMath2 = {
PI: 3.14,
square(num) {
return num * num;
},
cube(num) {
return num * num * num;
},
};
const square = {
area(side) {
return side * side;
},
perimeter(side) {
return side * 4;
},
};
console.log(square.area(10));
console.log(square.perimeter(10));
thisについて
thisの値はthisを使っている関数が、どのように呼ばれたか。に依存する。
// this
// このオブジェクト内が必ず参照されるわけではない!
const cat = {
name: "タマ",
color: "gray",
breed: "アメショ",
cry() {
// console.log(name); // 出力されない🙅♀️
console.log(this);
console.log(`${this.name}がにゃーと鳴く`); // 出力される🙆
},
};
// .の左側にあるものがthisになる
const cry2 = cat.cry;
try & catch
プログラムの実行を継続的に可能にする。
try-catchを使用することで、エラーが起きた場合はcatchの中の処理が実行される。
エラーの内容も引数に取って確認することもできる。errorなどの名前にしたり。
// エラーが起きた場合の処理、エラーが起きると処理がそこで終わってしまう。
try {
hello.tuUpperCase();
} catch {
console.log("エラー");
}
console.log("処理の続き");
// catch(e)とすることで、エラー内容を確認することもできる。
function shout(msg) {
try {
console.log(msg.toUpperCase().repeat(3));
} catch (error) {
console.log(error);
console.log("文字列を入れてね");
}
}
shout("hello");
shout(1234); // 数字が入るとcatchの処理に移る
for eachメソッド
配列のメソッド。配列の中身を1個づつ取り出して処理をすることができる。
ただ現代ではfor of の使用を躊躇するケースが少ない(IEのサ終)ので、基本的にfor ofを使用した方が良さそう、コードの処理の見通しも良い。列挙可能なオブジェクトに対しても使える。
for eachはあくまでも"配列"のメソッド。
// forEach、あくまで配列のメソッド、stringには使用できない。
const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// 配列の中身を1個づつ取り出して処理をする
numbers.forEach(function (el) {
console.log(el);
});
// // for ofでも同じことはできるしわかりやすい?
for (let elem of numbers) {
console.log(elem);
}
const movies = [
{ title: "Amadeus", score: 99 },
{ title: "Bool", score: 80 },
{ title: "Candy", score: 79 },
{ title: "Eden", score: 59 },
];
// for each ver
movies.forEach(function (movie) {
console.log(`${movie.title} - ${movie.score} / 100`);
});
// for ofでも同じことはできる。
for (let movie of movies) {
console.log(`${movie.title} - ${movie.score} / 100`);
}
mapメソッド
元の配列があって、その中の要素にそれぞれ処理を加えて、新しい配列にマッピングして新しい配列を作成することができる。React開発において重要なメソッド。
配列内の全要素に対する一貫した変換
// mapメソッド
// for eachと違うのは、returnして処理された各々の値・要素で、新しい配列にマッピングすることができる。
const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
const doubles = numbers.map(function (num) {
return num * 2;
});
console.log(doubles); // [2,4,6,8,10,12,14,16,18,20]
const movies = [
{ title: "Amadeus", score: 99 },
{ title: "Bool", score: 80 },
{ title: "Candy", score: 79 },
{ title: "Eden", score: 59 },
];
const newMovies = movies.map(function (movie) {
return movie.title;
});
console.log(newMovies);
アロー関数
関数を書きやすくしたもの。
暗黙的returnを使用する際は、式が1つだけの時に有効。
// 暗黙的リターンは一つの式だけの時有効。
const add = (x, y) => x + y;
console.log(add(3, 7));
const movies = [
{ title: "Amadeus", score: 99 },
{ title: "Bool", score: 80 },
{ title: "Candy", score: 79 },
{ title: "Eden", score: 59 },
];
const newMovies = movies.map((movie) => {
return `${movie.title} - ${movie.score} / 100`;
});
console.log(newMovies);
filterメソッドとmapメソッドは組み合わせ可能
filterメソッドは元の配列に変更を加えず、条件に合う要素だけを取り出して新しい配列を作成する。その後mapメソッドで、その配列の中の値だけを取り出して新しい配列を作成することができる。React開発でも使用する重要なこと。
const movies = [
{ title: "Amadeus", score: 99, year: 1999 },
{ title: "Bool", score: 82, year: 1997 },
{ title: "Candy", score: 79, year: 1980 },
{ title: "Eden", score: 100, year: 2024 },
{ title: "Youtube", score: 100, year: 1996 },
];
// mapメソッドと組み合わせも可能
// 配列moviesの中でscoreが80以上のものをfilterした新しい配列を作成し、mapメソッドでその配列のtitleだけを取り出して配列を作成する。
const goodMovie = movies
.filter((movie) => movie.score > 80 && movie.year > 1990)
.map((movie) => movie.title);
console.log(goodMovie);
const budMovie = movies.filter((movie) => {
return movie.score < 70;
});
// console.log(budMovie);
const validUserNames = (names) => {
return names.filter((name) => name.length < 10);
};
console.log(validUserNames(["taros", "tomtom", "testmessssssss", "tommy"]));
day9 11/30
reduce
特徴
- 配列の要素を1つずつ処理して、単一の値にまとめる
- その「単一の値」は、数値、配列、オブジェクトなど何でもOK
- 前回の処理結果を次の処理に活用できる
基本的な構文
array.reduce((accumulator, currentValue) => {
// 処理
return accumulator;
}, initialValue);
// accumulator: 蓄積値(前回の処理結果)
// currentValue: 現在処理している配列の要素
// initialValue: 初期値(省略可能)
const apiResponse = [
{ id: 1, status: "完了" },
{ id: 2, status: "未完了" },
{ id: 3, status: "未完了" },
{ id: 4, status: "完了" },
{ id: 5, status: "完了" },
{ id: 6, status: "完了" },
{ id: 7, status: "未完了" },
];
const displayData = apiResponse.reduce((acc, item) => {
// ステータスごとにグループ化
acc[item.status] = acc[item.status] || [];
acc[item.status].push(item);
return acc;
}, {});
// displayData の中身
// {
// "完了": [
// { id: 1, status: "完了" },
// { id: 4, status: "完了" },
// { id: 5, status: "完了" },
// { id: 6, status: "完了" }
// ],
// "未完了": [
// { id: 2, status: "未完了" },
// { id: 3, status: "未完了" },
// { id: 7, status: "未完了" }
// ]
// }
関数の種類におけるthisのスコープの決まり方
アロー関数と通常関数では、thisのスコープの決まり方が違う。
- 通常の関数は、呼び出し時に新しいthisバインディングを作成する
- アロー関数は自身のthisバインディングを持たない。代わりに、定義された時点での外側のスコープのthisを維持する
const user = {
name: "田中",
// メソッドの定義
greet: function () {
// setTimeout のコールバックで通常の関数を使用
setTimeout(function () {
console.log(`こんにちは、${this.name}です`);
// this.name は undefined になる
// なぜなら、この function() のスコープでの this はグローバルを指すため
}, 1000);
},
// 別のメソッド - こちらは正しく動作する例
greetCorrect: function () {
// アロー関数を使用
setTimeout(() => {
console.log(`こんにちは、${this.name}です`);
// "こんにちは、田中です" と出力される
// アロー関数は親スコープの this を維持するため
}, 1000);
},
};
デフォルト引数の設定
引数に値が入ってこない場合でも動作するように、デフォルトの引数を設定することができる。
numSize = 6
のように = で結ぶだけでOK,
順番には注意。
// デフォルト引数の設定
function rollDie(numSize = 6) {
return Math.floor(Math.random() * numSize + 1);
}
console.log(rollDie()); // 引数を設定しなかった場合、デフォルト値の6が適用される
const greed = (msg = "こんにちわ", person = "誰か") => {
console.log(`${msg}、${person}さん`);
};
// 飛ばしたい時はundefinedを使用する
console.log(greed(undefined, "田中"));
スプレッド構文
列挙可能なオブジェクトなら展開可能、React開発においてめちゃくちゃ重要な概念、よく使う。
配列やオブジェクトを直接編集するのではなく、コピーを作成してプロパティを追加できるので、イミュータビリティ(不変性)が保たれる。
// スプレッド構文
// 列挙可能なオブジェクトなら展開可能
const nums = [13, 4, 5, 6, 9, 56];
console.log(Math.max(...nums)); // 出力:中に入るのはnumsの配列の値、配列を展開できる。
console.log(...nums); // 出力:13 4 5 6 9 56
const str = "Hello";
console.log(...str); // H e l l o 1文字づつ展開される
// 配列リテラル
const num1 = [1, 2, 3, 4, 5, 6];
const num2 = [7, 8, 9, 10, 11, 12];
const newArray = [...num1, 100, 101, 102, 103, ...num2]; // 新しい配列を合体して作成できる
console.log(newArray);
// 元の配列は変更せず、新しく作成される配列は別の配列として作成される
const num1Copy = [...num1]; // 1,2,3,4,5,6と中身は同じ配列だが、、、
console.log(num1 === num1Copy); // falseになる
// stringを展開して一文字づつを値にした配列を作成することも可能
const string = "あいうえお";
const array = [...string];
console.log(array); // あ、い、う、え、お
// オブジェクトリテラルの場合
const feline = {
legs: 4,
family: "猫科",
};
const canine = {
family: "犬科",
bark: true,
};
const cat = { ...feline, color: "black" };
// console.log(cat);
// 合体可能だが、共通のプロパティのfamilyは後から展開した方の値に上書きされる。。。
const catDog = { ...feline, ...canine };
// console.log(catDog);
// 例)ユーザーが入力したデータを受け取る
const formData = {
email: "xxx@gmail.com",
password: "secret",
username: "John",
};
// 元のオブジェクトformDataを直接更新するのではなく、新しいオブジェクトuserとして、formDataをコピーして、その後にプロパティを追加していく。
const user = { ...formData, id: 123, isVerified: false };
console.log(user);
console.log(formData);
レスト構文
レスト(残り物)の意味。スプレッド構文に似ているが、
可変長の引数を扱う場合に有効。
引数の残りを全部まとめてくれる
// レスト構文 残余引数、スプレッド構文に似ているが違うもの
function sum() {
return arguments.reduce((total, num) => total + num);
}
// sum(1, 2, 3);
function sum(...nums) {
console.log(nums);
return nums.reduce((total, num) => total + num);
}
sum(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
// 残りのものに引数が入る
function runResult(gold, silver, ...rest) {
console.log(`金: ${gold}`);
console.log(`銀: ${silver}`);
console.log(`その他: ${rest}`);
}
// 第一引数に"田中くん",第二引数に"マイケル"それ以降の引数はすべてrestの引数に配列として入る。
runResult(
"田中くん",
"マイケル",
"Johnくん",
"smithさん",
"エミリー",
"ミッシェル"
);
function calculateTotal(shopName, date, ...prices) {
console.log(shopName); // 'スーパー' (string)
console.log(date); // '2024-01-01' (string)
console.log(prices); // [200, 300, 500, 1000] (number[]配列)
// prices配列に対してreduceを使って合計を計算
const total = prices.reduce((sum, price) => sum + price, 0);
// 1周目: sum(0) + price(200) = 200
// 2周目: sum(200) + price(300) = 500
// 3周目: sum(500) + price(500) = 1000
// 4周目: sum(1000) + price(1000) = 2000
return `${date}の${shopName}での合計金額は${total}円です。`;
分割代入
分割代入で配列の中身を変数として定義するのが簡単になる。
特にオブジェクトの分割代入はReactでも頻繁に使用するので重要な概念。
// 分割代入
const scores = [12345, 34234, 4343, 344343, 99999, 34343];
// 分割代入を使用すると、変数の順番に配列の順が入っていく。
const [gold, silver, bronze] = scores;
console.log(gold);
// レスト構文を使用すると残りの値を格納することもできる
const [golden, ...rest] = scores;
console.log(...rest);
// オブジェクトの分割代入:頻度高め、使用したい情報だけを取り出して使用することが多い。
// オブジェクトから個別のプロパティを変数に割り当てることができる
// 基本プロパティ名と一緒、変数名を指定することもできる
const user = {
email: "string@gmail.com",
password: "string0000",
firstName: "taro",
lastName: "yamada",
born: 1999,
died: 2100,
city: "NewYork",
state: "California",
};
const user2 = {
email: "string@gmail.com",
password: "string0000",
firstName: "taro",
lastName: "yamada",
born: 1999,
city: "NewYork",
state: "California",
};
const { firstName, lastName, email } = user;
// :の後に変数名を指定すると変数の名前も変更することができる
const { born: birthYear } = user;
console.log(birthYear); // 1999
// 変数の初期値の設定もできる
const { died = "N/A" } = user2;
console.log(died);
関数パラメーターの分割代入
関数に渡ってくる、パラメーターの段階で分割代入をすることができる。めっちゃ便利で楽にかける。Reactでめちゃ使う。
// 関数パラメーターの分割代入
const user = {
email: "string@gmail.com",
password: "string0000",
firstName: "taro",
lastName: "yamada",
born: 1999,
died: 2100,
city: "NewYork",
state: "California",
};
const user2 = {
email: "string@gmail.com",
password: "string0000",
firstName: "John",
lastName: "Smith",
born: 1999,
city: "NewYork",
state: "California",
};
// 引数として渡ってきたuserオブジェクトは、パラメーターの段階で、分割代入される
// Reactで頻繁に使用
function fullName({ firstName, lastName }) {
return `${firstName} ${lastName}`;
}
console.log(fullName(user2));
const movies = [
{ title: "Amadeus", score: 99 },
{ title: "Bool", score: 80 },
{ title: "Candy", score: 79 },
{ title: "Eden", score: 59 },
{ title: "titan", score: 92 },
];
// コールバック関数でも分割代入を使用可能 scoreプロパティをそのまま使用している
const hightMovies = movies.filter(({ score }) => score >= 90);
console.log(hightMovies);
// map関数を使用
const review = movies.map(({ title, score }) => {
return `${title}は${score}点です。`;
});
console.log(review);
day10 12/1
DOM操作の基本
IDやタグ、クラス名で要素を取得すると、HTMLコレクションという配列っぽいものを返してくれる。
HTMLコレクション(HTMLCollection):DOMの変更をリアルタイムに反映する特別なオブジェクト
だが現代では、querySelectorやquerySelectorAllを使用する方が一般的。
// HTMLからIDを取得する
const banner = document.getElementById("toc");
// HTMLからタグをすべて取得する
const allImg = document.getElementsByTagName("div");
// for-ofでループを回すこともできる。
for (let img of allImg) {
img.src =
"https://upload.wikimedia.org/wikipedia/commons/thumb/7/75/Partridge_Silkie_hen.jpg/900px-Partridge_Silkie_hen.jpg";
}
// HTMLからクラス名をすべて取得する
const classes = document.getElementsByClassName("square");
for (let img of classes) {
img.src =
"https://upload.wikimedia.org/wikipedia/commons/thumb/7/75/Partridge_Silkie_hen.jpg/900px-Partridge_Silkie_hen.jpg";
}
現代は要素の取得はquerySelectorやquerySelectorAllを使用する方が一般的。
// 最初に見つけた要素を一つだけ返す、属性も使用できる。aタグのtitle属性とか。
const selector = document.querySelector("a[title='ヒツジ']");
// 当てはまる全ての要素を取得できる
// aタグのlink要素だけを取得して表示する
const links = document.querySelectorAll("p a");
for (let link of links) {
console.log(link.href);
}
console.log(links);
innerText,textContent,innerHTMLの違い
innerText
- 人間が見えるテキストのみを取得
- CSSで非表示の要素は無視
- CSSのスタイリングを考慮(例:display: noneの要素は含まない)
- パフォーマンスに若干の影響あり(レンダリングが必要なため)
textContent
- スクリプトタグやスタイルタグの中身も含めて、全てのテキストを取得
- CSSで非表示になっている要素も取得
- 改行やスペースをそのまま保持
innerHTML
- HTML要素を含めて取得・設定ができる
ただテキストが欲しい → textContent
画面に見えてるものだけ欲しい → innerText
HTMLタグも含めて全部欲しい → innerHTML
// // 取得した要素の操作。コンセプトを理解する。
const h1 = document.querySelector("h1");
// textContentはnode内の全てのコンテンツを取得する
// innerTextはページ上で見えてるものを取得する。
const allLink = document.querySelectorAll("a");
for (let link of allLink) {
link.innerText = "私はリンクです!!!";
}
// p要素の中の全てのHTML要素が取得できる。html要素を含めて更新
const head = (document.querySelector("h1").innerHTML =
"<b>ううううわおお!!!!</b>");
console.log(head);
day11 12/2
属性について
attribute = 属性の意味
属性を取得して変更をすることができる。
// 属性操作について。
// 取得する形式が違うかったりするので注意が必要。
// getAttribute
const firstLink = document.querySelector("a");
console.log(firstLink.getAttribute("href")); // /wiki/%E5%BA%AD 相対パス,HTMLの要素そのまま
const firstLinks = document.querySelector("a").href;
console.log(firstLinks); // http://127.0.0.1:5500/wiki/%E5%BA%AD 絶対パス,完全なURL
// setAttributeで取得した属性に対して新しい値をセットすることもできる
console.log(firstLink.setAttribute("href", "https://google.com"));
const eggImg = document.querySelector("img");
eggImg.setAttribute("src", "https://devsprouthosting.com/images/chicken.jpg");
eggImg.setAttribute("alt", "chicken");
console.log(eggImg);
CSSスタイルを変更する
// CSSスタイルを変更する
const h1 = document.querySelector("h1");
console.log(h1.style); // これでCSSを確認しても、CSSファイルで設定されたCSSは当たっていない、インラインスタイルのCSSだけが確認できる。
console.log((h1.style.color = "green")); // JSで直接スタイルを変える
console.log((h1.style.fontSize = "1.8rem")); // 必ずstringでプロパティを定義する必要がある。
const links = document.querySelectorAll("a");
for (let link of links) {
link.style.color = "pink";
link.style.textDecorationColor = "magenta";
link.style.padding = "0 2em";
}
const div = document.querySelector("#container");
const divImg = document.querySelector("img");
div.style.textAlign = "center";
divImg.style.width = "150px";
divImg.style.borderRadius = "50%";
// 配列の中にcolorのプロパティの値を格納し、取得したspan要素に順番に色を当ててレインボーにする。
const colors = ["red", "orange", "yellow", "green", "blue", "indigo", "violet"];
const strings = document.querySelectorAll("span");
let i = 0;
for (let string of strings) {
string.style.color = colors[i];
i++;
}
classList
クラスの付与・削除・つけ外しをより簡単にすることができる。
- .classList.add("クラス名"); クラス付与
- .classList.remove("クラス名"); クラス削除
- .classList.toggle("クラス名"); クラスの付け外し
const h2 = document.querySelector("h2");
// クラスを追加していくことができる
h2.classList.add("pink");
h2.classList.add("border");
// クラスを外すこともできる
h2.classList.remove("border");
// クラスの付け外しをすることもできる、ハンバーガーメニューのボタンとか
// 今の状態がそのクラスがついているかどうかを判断してtoggleさせている
h2.classList.toggle("pink");
const listItem = document.querySelectorAll("li");
for (let item of listItem) {
item.classList.toggle("highlight");
}
親・子要素を取得、次・前の要素を取得する
const firstBold = document.querySelector("b");
// 親要素を辿ることができる
console.log(firstBold.parentElement);
// 子要素も同様に辿れる
console.log(firstBold.children);
const squareImg = document.querySelector(".square");
// 次の要素を取得する
console.log(squareImg.nextElementSibling); // 実際の兄弟"要素"が返ってくる
// console.log(squareImg.nextSibling); // ノードが返ってくる
// 前の要素を取得する
console.log(squareImg.previousElementSibling);
要素を作成してHTML要素に挿入する
appendChild ver
- 一つずつしか追加できない
- テキストを追加するには追加手順が必要
append ver
- 複数のものを一度に追加できる
- テキストを直接追加できる
// 要素を作成してHTMLに挿入する
// appendChild ver
const newImg = document.createElement("img");
console.log(
(newImg.src =
"https://plus.unsplash.com/premium_photo-1732736768058-42f76dc6e6e3?w=800&auto=format&fit=crop&q=60&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxmZWF0dXJlZC1waG90b3MtZmVlZHw1fHx8ZW58MHx8fHx8")
);
newImg.classList.add("square");
document.body.appendChild(newImg);
// 新しいh3要素を作成して、
const newH3 = document.createElement("h3");
// h3の中身を更新して
newH3.innerText = "これは新規追加のh3です!!";
// body要素等に挿入する
document.body.appendChild(newH3);
// append() ver
// 最後に直接挿入することができる
const p = document.querySelector("p");
p.append(
"Whooooooooooooooooooooooooo",
"aaaaaaaaaaaaaaaaa",
"eeeeeeeeeeeeeeeee"
);
const newB = document.createElement("b");
// 先頭に直接追加する
newB.append("やっぴーー");
p.prepend(newB);
// 兄弟要素に追加する
const newH2 = document.createElement("h2");
newH2.append("鶏だよ");
const h1 = document.querySelector("h1");
h1.insertAdjacentElement("afterend", newH2); // (どこのポジションに, 何を追加するか)
const h3 = document.createElement("h3");
h3.innerText = "これはH3";
newH2.after(h3);
const container = document.querySelector("#container");
// for文でid:containerの中にボタン作成を100回繰り返す。
for (let i = 0; i < 100; i++) {
const newButton = document.createElement("button");
newButton.innerText = `ボタン${i + 1}`;
container.appendChild(newButton);
}
要素を削除する
remove()を使用して自分自身を削除する。
removeChild()もあるが、IE無き今は特定のユースケースがない限りはremove()で良さそう。
// remove()を使用すれば削除される自分自身だけ考えればOK
const img = document.querySelector("img");
img.remove();
ポケモン図鑑を作ってみる
JavaScriptを使用して簡易的なポケモン図鑑を作ってみる。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>ポケモン図鑑</title>
<link rel="stylesheet" href="app.css" />
</head>
<body>
<h1>ポケモン図鑑</h1>
<section id="container"></section>
<script src="app.js"></script>
</body>
</html>
#CSS
.pokemon {
display: inline-block;
text-align: center;
}
.pokemon img {
display: block;
}
const container = document.querySelector("#container");
const baseUrl =
"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/";
for (let i = 1; i <= 1025; i++) {
const pokemon = document.createElement("div"); // 外側のdiv
pokemon.classList.add("pokemon");
const label = document.createElement("span"); // 番号を表示するためのlabel
label.innerText = `#${i}`; // ラベルのテキストを#1,#2,#3...のようにテキストを挿入
const newImg = document.createElement("img"); // 画像を表示するためのimg
newImg.src = `${baseUrl}${i}.png`;
// divの中に作詞したlabel,newImgを入れる
pokemon.appendChild(newImg);
pokemon.appendChild(label);
// id:containerの中に作成されたdiv要素自体を入れる
container.appendChild(pokemon);
}
DOMイベント
ユーザーの入力やアクションに反応する。
イベントの登録は基本的にaddEventlisterを使用する。この方が複数のイベントを登録できたり、第三引数でoptionを設定できたりと柔軟な対応ができるため。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>JSEvent</title>
</head>
<body>
<h1>Event</h1>
<!-- htmlに直接記述することもできるが使い回ししづらいなどあるので非推奨 -->
<button onclick="console.log('クリックされました!');">Click Me</button>
<button id="v2">Click JS ver</button>
<button id="v3">Click JS v3</button>
<button id="hello">Hello</button>
<button id="goodbye">Goodbye</button>
<script type="module" src="app.js"></script>
</body>
</html>
const btn = document.querySelector("#v2");
// クリックイベントを登録する
btn.onclick = function () {
console.log("クリックされたよ!!");
};
function scream() {
console.log("入ったよ!!");
}
// このやり方だと、複数のイベントを登録できない。。。
btn.onmouseenter = scream;
btn.onclick = function () {
console.log("2個目の処理");
};
const h1 = (document.querySelector("h1").onclick = function () {
console.log("h1だよ!");
});
// addEventListener(推奨),第3引数を設定して、オプションを設定できる。1回だけの呼び出しにしたりとか。
const button = document.querySelector("#v3");
button.addEventListener(
"click",
function () {
alert("発火!!");
},
{ once: true }
);
const helloBtn = document.querySelector("#hello");
const byeBtn = document.querySelector("#goodbye");
helloBtn.addEventListener("click", function () {
console.log("hello");
});
byeBtn.addEventListener("click", function () {
console.log("goodbye");
});
イベントとthis
イベントの中のコールバック関数ではthisはその関数の設定された要素になる。
// ランダムなrgb()の値を生成するための関数
const makeRandomColor = () => {
const r = Math.floor(Math.random() * 256);
const g = Math.floor(Math.random() * 256);
const b = Math.floor(Math.random() * 256);
return `rgb(${r},${g},${b})`;
};
// イベントの中のコールバック関数の中ではthisはその関数がコールバックとして設定されている要素になる。
const buttons = document.querySelectorAll("button");
for (let button of buttons) {
button.addEventListener("click", colorRize);
}
const h2s = document.querySelectorAll("h2");
for (let h2 of h2s) {
h2.addEventListener("click", colorRize);
}
function colorRize() {
this.style.backgroundColor = makeRandomColor();
this.style.color = makeRandomColor();
}
day12 12/5
イベントのバブリング
イベントが上に泡のように上がっていってしまう現象。
e.stopPropagation();のプロパティを使用すれば、これ以上イベントが伝播しないように設定できる。
#index.html
<div id="container">
クリックして隠す
<button id="changeColor">色を変える</button>
</div>
<style>
.hide {
display: none;
visibility: hidden;
}
</style>
#app.js
const button = document.querySelector("#changeColor");
const container = document.querySelector("#container");
button.addEventListener("click", function (e) {
e.stopPropagation(); // バブリングをストップするプロパティ
container.style.backgroundColor = makeRandomColor();
});
container.addEventListener("click", function () {
container.classList.add("hide");
});
const makeRandomColor = () => {
const r = Math.floor(Math.random() * 255);
const b = Math.floor(Math.random() * 255);
const g = Math.floor(Math.random() * 255);
return `rgb(${r},${g},${b})`;
};
イベントデリゲーション
子要素で発生したイベントを親要素が処理をすることができる。
// フォームのイベント
const tweetForm = document.querySelector("#tweetForm");
// 送信した時のイベント
tweetForm.addEventListener("submit", function (e) {
e.preventDefault(); // デフォルトの挙動を止める,formはデフォルトでページ遷移してしまうので止める。
// formのelementsの中のname属性のvalueを取得する
const userNameInput = tweetForm.elements.username;
const postInput = tweetForm.elements.tweet;
addPost(userNameInput.value, postInput.value);
// 送信後はinputの中身を空にする
userNameInput.value = "";
postInput.value = "";
});
// 受け取った値で新しいポストを作成する処理
const addPost = (userNameInput, postInput) => {
const newPost = document.createElement("li");
const bTag = document.createElement("b");
const tweets = document.querySelector("#tweets"); // ポストを表示する場所
bTag.append(userNameInput); // bタグの中にuserNameInputで取得した値を入れる
newPost.append(bTag); // newPostの中にbTagを挿入する
newPost.append(` - ${postInput}`); // newPostの中にpostInputで取得した値を入れる
tweets.append(newPost); // tweetsの中にnewPostを挿入する
};
// イベントデリゲーション
// 要素自身ではなく、親要素に処理を任せる
tweets.addEventListener("click", function (e) {
if (e.target.nodeName === "LI") {
e.target.remove();
} else if (e.target.nodeName === "B") {
e.target.parentElement.remove();
}
});
卓球得点表をJavaScriptで作成する
プレイヤー情報をオブジェクトで管理して関数に切り出して管理する。
const p1 = {
score: 0,
button: document.querySelector("#p1Button"),
display: document.querySelector("#p1Display"),
};
const p2 = {
score: 0,
button: document.querySelector("#p2Button"),
display: document.querySelector("#p2Display"),
};
const resetButton = document.querySelector("#reset");
const winningScoreSelect = document.querySelector("#winningScore");
let winningScore = 3;
let isGameOver = false;
function updateScores(player, opponent) {
if (!isGameOver) {
player.score += 1;
player.display.textContent = player.score;
if (player.score === winningScore) {
isGameOver = true;
player.display.classList.add("winner");
opponent.display.classList.add("loser");
player.button.disabled = true;
opponent.button.disabled = true;
}
}
}
p1.button.addEventListener("click", function () {
updateScores(p1, p2);
});
p2.button.addEventListener("click", function () {
updateScores(p2, p1);
});
// 勝利得点の変更
winningScoreSelect.addEventListener("change", function () {
winningScore = parseInt(this.value);
reset();
console.log(typeof winningScore);
});
// リセットボタン
resetButton.addEventListener("click", reset);
// 得点のリセット処理関数
function reset() {
isGameOver = false;
// p1,p2それぞれに同じ処理を加える
for (let p of [p1, p2]) {
p.score = 0;
p.display.textContent = 0;
p.display.classList.remove("winner", "loser");
p.button.disabled = false;
}
}
day13 12/8
コールスタックについて
Chromeのデバッグツールを使用してコールスタックの処理順、積み重なっていくのを確認することができる。
const multiply = (x, y) => x * y;
const square = (x) => multiply(x, x);
const isRightTriangle = (a, b, c) => square(a) + square(b) === square(c);
console.log("before!!!");
isRightTriangle(3, 4, 5);
console.log("after!!!");
JavaScriptはシングルスレッド
JavaScriptでは一度に一つの作業しかできない。
もし重い処理だったらサイト固まる???
回避策はある、コールバック関数を使用する!
なぜsetTimeout関数を使用するとsetTimeoutのカウントをしつつその先の処理も実行できるのか?
→ブラウザが処理をしてくれている。
- ブラウザはWeb APIと呼ばれるバックグラウンドで処理を実行してくれる機能(リクエスト・setTimeoutなどの処理)を提供してくれる。
- JavaScriptのコールスタックはこのWeb APIを認識すると、ブラウザに処理を依頼する
- ブラウザが処理を終えると、コールバックとしてスタックに積まれる
何がいいのか?
メインスレッドがブロックされることなく処理を続行できる
重い処理や時間のかかる処理を別で実行できる
UIの応答性を保ったまま非同期処理を実行できる
console.log("サーバーにリクエストを送信"); // 最初に実行される処理
// この処理に来た時点で、ブラウザにsetTimeoutで3秒測るように依頼だけをして次に進む、タイマーの管理はブラウザーがする
setTimeout(() => {
console.log("サーバーからレスポンスが来ました!"); // 最後に実行される処理、3秒経過後にブラウザからコールバックを実行するように依頼が来る
}, 3000);
console.log("ここがファイルの末端"); // 2番目に実行される処理
コールバック地獄、救済としてのPromise
JavaScriptはシングルスレッドなので、JavaScript自体は複数の処理を同時にできない。コールバック関数を使用すれば回避することはできる、が、コールバック関数をネストしすぎるとコードの見通しが悪くなってしまう。
現代はPromise, async/await を使用して記述する。
// コールバックがネストしまくるとコードの見通しが悪くなる。。。
setTimeout(() => {
document.body.style.background = "red";
setTimeout(() => {
document.body.style.background = "orange";
setTimeout(() => {
document.body.style.background = "green";
setTimeout(() => {
document.body.style.background = "blue";
}, 1000);
}, 1000);
}, 1000);
}, 1000);
day14 12/9
PROMISEを使用した非同期処理
PROMISE(約束)はオブジェクト
未来のある時点で値を持つ約束をするオブジェクト
未来の出来事で、成功しても失敗しても何かしらの結果をもらう。
PROMISEには3つの状態がある
- pending(待機):初期状態
- fulfiled(成功):処理が成功して完了した状態
- rejected(失敗・拒絶):処理が失敗した状態
PROMISEオブジェクトに、成功した場合のコールバック関数、
失敗した場合のコールバック関数を登録することができる。
then(それから)は前のpromiseが実行された後に実行される。
return でpromiseを返すことによって、次のthenに繋げることができる。こうすることでコードの見通しも良くなる。
// promise
const fakeRequestPromise = (url) => {
return new Promise((resolve, reject) => {
const delay = Math.floor(Math.random() * 4500) + 500;
setTimeout(() => {
if (delay > 4000) {
reject("コネクションタイムアウト");
} else {
resolve(`ダミーデータ(${url})`);
}
}, delay);
});
};
fakeRequestPromise("yelp.com/page1")
// 1個目のリクエスト
.then((data) => {
console.log("成功1");
console.log(data);
return fakeRequestPromise("yelp.com/page2"); // thenの中でpromiseを返すことで、次のthenに繋げることができる
})
// 2個目のリクエスト
.then((data) => {
console.log("成功2");
console.log(data);
return fakeRequestPromise("yelp.com/page3");
})
// 3個目のリクエスト
.then((data) => {
console.log("成功3");
console.log(data);
})
// どこかで失敗した場合の処理
.catch((error) => {
console.log(error);
console.log("失敗!");
});
Promiseの作成
resolve:成功した時に呼ぶ関数
reject:失敗したときに呼ぶ関数
promiseは一度resolveもしくはrejectされるとその状態は変更できない。
もし変更できてしまうと、APIなどで取得したデータの整合性が保てない。
どの結果を信頼していいか分からなくなり、デバックも困難になってしまうため。
自分でpromiseを定義する場面は少ないが、何をしているのか、どんな処理なのかを押さえている必要がある。作るよりも、使うことに慣れる!
// promiseの作成
const fakeRequest = (url) => {
return new Promise((resolve, reject) => {
const rand = Math.random();
setTimeout(() => {
if (rand < 0.7) {
resolve("dummy-data"); // 成功の場合に返す値
return; // resolveの後はreturnで関数の処理を終わらせる。resolveの後にrejectを実行させないため
}
reject("コネクションタイムアウト"); // 失敗の場合に返す値
}, 1000);
});
};
fakeRequest("/hoge/hoge1")
.then((data) => {
console.log("成功", data);
})
.catch((err) => {
console.log("エラー!", err);
});
async function
非同期(async)な処理をもっとすっきりと書ける新しい構文。
キーワード async / await
async(非同期)
- 関数の前につけて、この関数は非同期な関数であることを宣言する。
- asyncな関数は必ず自動的にPromiseを返す
- 関数が値を返せば、Promiseはその値でresolveする
- 関数がエラーをthrowした場合、Promiseはそのエラーでrejectする
- return文はPromise.resolve()として扱われる
- throw文はPromise.reject()として扱われる
- このため、.then()と.catch()でハンドリングできる
// asyncをつけることでPromiseにできる
const sing = async () => {
return "らららららら"; // Promiseを返す
};
// Promiseを返すのでthen()が使える、dataの中には Promiseのresolve(成功時の値)が入ってくる
sing()
.then((data) => {
console.log("成功:", data); // 結果:成功:らららららら
})
.catch((err) => {
console.log("エラ−!!", err);
});
const login = async (username, password) => {
if (!username || !password) {
throw new Error("ユーザー名またはパスワードがありません");
}
if (password === "secret") {
return "ようこそ!!!";
}
throw new Error("パスワードが間違ってます");
};
await(待つ)
非同期なコードを同期的なコードのように書くことができる
- awaitはasync関数の中でしか使えない
- awaitはPromiseがresolveまたはrejctするまでasync関数の実行を一時的に停止する
リクエストをawaitしてその値を変数に格納するのは使用頻度かなり高い!
const fakeRequest = (url) => {
return new Promise((resolve, reject) => {
const delay = Math.floor(Math.random() * 4500) + 500;
setTimeout(() => {
if (delay > 4000) {
reject("コネクションタイムアウト"); // 失敗時の処理
} else {
resolve(`ダミーデータ(${url})`); // 成功時の処理
}
}, delay);
});
};
async function makeRequest() {
try {
// この中のawaitで発生したエラー(reject)は全てcatchされます
const data1 = await fakeRequest("/hoge1"); // リクエストをawaitして変数に格納する
console.log(`data1: ${data1}`);
const data2 = await fakeRequest("/hoge2"); // リクエストをawaitして変数に格納する
console.log(`data2: ${data2}`);
} catch (e) {
// どちらかのリクエストでrejectされた場合、
// eにはrejectで渡された"コネクションタイムアウト"が入ってくる
console.log("エラー発生!!!", e);
}
}
makeRequest();
AJAX
WEBサイトが表示されている裏側でリクエストを投げて情報を取得したり送信したりする。
例)
- unsplashで下にスクロールをするとどんどん画像が読み込まれていくみたいな。
スクロールを検知して処理がされる。新しい画像を取得して要素を作って表示している。 - 検索窓の入力をすると即座にサジェストがぬるっと出てくるような
AJAXで返ってくる情報はデータそのもの。
API(アプリケーションプログラミングインターフェース)
WEBエンジニアの言うAPIは大抵WebAPIのことを指す。
世の中に公開されているAPIがたくさんある、APIの中でもエンドポイントが接点となる。
無料公開のAPIもあれば、使用料の制限があるAPIも存在する。
JSONデータについて
ただのデータだけが含まれている。
JSON形式 or XML形式
- JSON:ソフトウェア同士で情報の取りをするための共通のテキストベースのフォーマット。
JSONにはundefinedという値がない、JavaScriptのオブジェクトに似ているが違うもの。
JSONを取得して、JavaScript等(Python, JAVA)に変換して使用する。共通のデータ。
APIリクエスト
APIテストツール
- POSTMAN(ちょい重いが機能が多い)
- HOPSCOTCH(軽くて速いが機能は少なめ)
ベースとなるURLとクエリーストリングは性質が異なる
- ベースURL: https://swapi.dev
- プロトコル(https)とドメイン名
- パス: api/people/5
- リソースの場所を示す
- 階層構造を表現できる
- パスパラメータ(この例では5)を含むことができる
- クエリーストリング: ?color=green&size=large
- ? から始まる、キー=値 の形式、複数のパラメータは & で連結
- オプショナルな追加情報を渡すのに使用
クエリーパラメーターの使用用途
オプショナルな条件
フィルタリング
ソート条件
Fetch API
昔: コールバックで書いていた
現代その1:Promise/fetch
現代その2:async/await
// Promise / Fetchを使用ver.
fetch("https://swapi.dev/api/people/1/") // Promiseを返してくれる
.then((res) => {
console.log("RESOLVE!", res);
return res.json(); // JSONを呼ぶとPromiseを返してくれるからthen()で繋げる
})
.then((data) => {
console.log(data); // レスポンスのbodyが使える
return fetch("https://swapi.dev/api/people/2/");
})
.then((res) => {
console.log("2個目のRESOLVE!", res);
return res.json(); // JSONを呼ぶとPromiseを返してくれるからthen()で繋げる
})
.then((data) => {
console.log(data); // レスポンスのbodyが使える
})
// エラーの場合の処理
.catch((err) => {
console.log("エラー", err);
});
// async / awaitを使用した最終形態ver.
const LoadStarWarsPeople = async (num) => {
try {
const res = await fetch(`https://swapi.dev/api/people/${num}/`); // 指定したURLにリクエストを送信、取得した値を変数 res に格納
const data = await res.json(); // Fetchで取得したレスポンスボディをJSONとして解析、データは変数 data に格納
console.log(data); // dataの中身は、res.json()メソッドが変換してくれた、JavaScriptオブジェクト
console.log(`私の好きなキャラクターは${data.name}です。`);
console.log(`このキャラクターの身長は${data.height}cmです。`);
} catch (e) {
console.log("エラー!!", e);
}
};
LoadStarWarsPeople(5);
Axiosライブラリを使用してもっと簡単にAPIリクエストを処理する
Axiosライブラリを使用することで、JSONパースの必要がなくなってもっとスッキリ記述することができる。
- res.json()の呼び出しが不要
- レスポンスデータに直接アクセス可能
小規模プロジェクトならasync/awaitで十分かもしれないが、大規模になってくると有効かも。
const getStarWarsPerson = async (id) => {
try {
const res = await axios.get(`https://swapi.dev/api/people/${id}/`); // リクエストを送信し、レスポンスを待つ。この時点でボディを取得できている
console.log(res.data.name); // JavaScriptのオブジェクト形式なのですぐにオブジェクトの中身にアクセスできる
} catch (e) {
console.log("ERROR", e);
}
};
getStarWarsPerson(5);
getStarWarsPerson(10);
APIを使用してボタンを押すたびにジョークが追加されるアプリケーション
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Axios</title>
</head>
<body>
<h1>クリックでジョーク</h1>
<button id="button">クリック</button>
<ul id="jokes"></ul>
<script src="https://cdn.jsdelivr.net/npm/axios@1.1.2/dist/axios.min.js"></script>
<script src="app.js"></script>
</body>
</html>
// ボタンを押すとジョークが追加されていくアプリケーション
const jokes = document.querySelector("#jokes");
const button = document.querySelector("#button");
// APIからジョークを取得するための非同期関数
const getDadJoke = async () => {
try {
// 第二引数のheadersの設定
const config = {
headers: {
Accept: "application/json",
},
};
const res = await axios.get("https://icanhazdadjoke.com", config); // 第二引数でheaderなどのoptionを設定することができる
return res.data.joke; // 取得したジョークをreturnする
} catch (e) {
console.log(e);
return "No Jokes Sorry...";
}
};
// 取得したジョークliに格納して、さらにulに格納する処理
const addNewJoke = async () => {
const jokeText = await getDadJoke(); // getDadJokeで取得したジョークを変数に格納
const newLi = document.createElement("Li"); // li要素を作成
newLi.append(jokeText); // liの中に取得したジョークテキストを挿入
jokes.append(newLi); // さらにulの中にliを格納
};
// ボタンをクリックするたびにジョークが追加される
button.addEventListener("click", addNewJoke);
TV番組を取得してその画像を表示する
const form = document.querySelector("#searchForm");
form.addEventListener("submit", async function (e) {
e.preventDefault();
const searchTermInput = form.elements.query;
const config = {
params: {
q: searchTermInput.value,
},
};
const res = await axios.get(`https://api.tvmaze.com/search/shows`, config);
makeImages(res.data);
searchTermInput.value = "";
});
const makeImages = (results) => {
for (let result of results) {
if (result.show.image) {
const img = document.createElement("IMG");
img.src = result.show.image.medium;
document.body.append(img);
}
}
};
ターミナル操作について
CUI: キャラクターユーザーインターフェース
キーボードからコマンドを使用して操作する
GUI: グラフィカルユーザーインターフェース
マウスなどを使用して視覚的に操作する
なぜターミナルを使用できた方がいいのか?
-
開発効率をあげる!
-
慣れればGUIよりも早く開発が可能になる、マウスのクリック、ドロップよりも速い
-
アクセス範囲
-
普段GUIではさわれないような領域にアクセスして操作することができる(取り扱い注意)
-
サーバーを実行したり止めたりの操作が可能になる
-
node.js,データベースを操作するのに必須!
ターミナルとは
コンピュータを操作するためのテキストベースのインターフェース
昔は端末のことを指していた。確かに和訳だと端末の意味。
例)ATMの端末がターミナルと言える
シェルとは
ターミナル上で動くプログラムのこと
例)ATM上で動いているソフトウェアがシェルと言える
BASHとは
シェルの一種、ちょっと前までMacの標準シェル
Node.jsとは?
JavaScriptのランタイム
ブラウザの外で実行できるランタイム
JavaScriptを使用してサーバーサイドのコードを書くことができる
何が作れるのか?
- Webサーバー
- コマンドラインツール
- ネイティブアプリ(VS codeとかSlackとかもNode.jsで作られている)
- ゲーム(あまり一般的ではない)
- ドローンのソフトウェア
...etc
Node.jsと通常のJavaScriptの違い
-
Node.jsにないもの
- ブラウザに関するもの。window, document, DOMのAPIは存在しない
-
Node.js特有のもの
- ブラウザに存在しないたくさんの組み込みモジュール、OS,ファイル、フォルダとのやり取りをサポートしてくれる
Node.jsでJavaScriptを実行する方法
node + JavaScriptファイル名
で記述されたJavaScriptが実行される。
for (let i = 0; i < 10; i++) {
console.log("Node.jsからこんにちは");
}
JavaScriptを記述して、ターミナルから下記コマンド実行(対象ディレクトリにいる必要あり。)
もしくは完全パスを渡す
node script.js
実行すると、ターミナルに出力される
Node.jsからこんにちは
Node.jsからこんにちは
Node.jsからこんにちは
Node.jsからこんにちは
Node.jsからこんにちは
Node.jsからこんにちは
Node.jsからこんにちは
Node.jsからこんにちは
Node.jsからこんにちは
Node.jsからこんにちは
npmを使用して簡単なアプリケーションを作成してみる
npmを組み合わせることで数行でアプリケーションを作成することができる。
package.jsonを使用することで、使用するnpmがdependenciesの部分に記録されていくので、node_modulesごと他人にコードを渡さなくていい(基本渡さないしGithubにもあげない)ようになる。
dependenciesは依存しているパッケージのこと。
package.jsonがあれば、開発者は【npm install】で必要なnode_modulesを一括でインストールできる。
// コマンドライン引数から入力を取得(3番目の引数)
const input = process.argv[2];
// francを使って言語コードを取得
const langCode = franc(input);
// 言語判定できなかった場合("und" = undefined)
if (langCode === "und") {
console.log("解析できません".red);
} else {
// 言語コードから言語名を取得して表示
const language = langs.where("3", langCode);
console.log(`${language.name}でしょうか?`.green);
}
Expressを理解する
ドキュメントを読んだ時にコードの意図がわかるようにする。
自分で0から書くというよりは処理を理解をする。
Expressとは?
npmのパッケージの一つ。Node.jsのためのWEBアプリ用のフレームワーク。
何ができるのか?
- リクエストを受けるサーバーの起動
- リクエストのパース(JavaScriptで扱えるように変換)
- リクエストとルーティングのマッピング
- HTTPレスポンスと関連コンテンツの作成
ルーティングについて
app.getを使用してパスに応じて処理を変えることができる。
以下の例はブラウザにresとしてテキストを表示する。
全てのパスで実行できる処理もあるが、順番に注意。
// ルーティング、パスに応じて処理を変える
app.get("/cats", function (req, res) {
res.send("にゃー!");
});
app.get("/dogs", function (req, res) {
res.send("わんわん");
});
app.get("/", function (req, res) {
res.send("ホームページ");
});
// ルーティングは定義した順番にマッチングしてくるので順番注意!、全部にマッチするワイルドカード的なもの
app.get("*", function (req, res) {
res.send("そんなパスはない!");
});
// POSTメソッドなど複数のメソッド使用可能
app.post("/cats", function (req, res) {
res.send("/catsにPOSTリクエストが来ました。");
});
Expressのルーティングにおける動的パラメータの仕組み
URLパスの中で :変数名 にすることで、動的なパラメーターを取得することができる。
その取得した値は、req.paramsオブジェクトのプロパティとして使用できる。
URLから取得したパラメーターを使用してデータベースから必要なデータを取得したり、フィルタリングをする際にとても有効。
// パターンとパラメーターを使用する
// 使用例:
// /r/gaming にアクセス → subreddit = "gaming"
app.get("/r/:subreddit", function (req, res) {
const { subreddit } = req.params;
res.send(`<h1>${subreddit}subredditのページ</h1>`);
});
// :を使用することで、パターンとパラメーターを使用する、この値からデータベースから何か取得したりできる。
// 使用例:
// /r/gaming にアクセス → subreddit = "gaming"
// ルーティングの中で超重要な仕組み
app.get("/:page/:postId", function (req, res) {
const { page, postId } = req.params;
res.send(`<h1>${page}subredditのpostIdが${postId}ページ</h1>`);
});
クエリーストリングを使用する
検索結果の表示のようなもの
URLの末尾に?キー:値 の形でデータを付加するもの。
検索、フィルタリング、ページネーションなどに使用
順序に依存しない
クエリストリングは特に検索機能やフィルタリング機能の実装において非常に重要な役割を果たす。
// クエリーストリングを使用する
app.get("/search", (req, res) => {
const { q } = req.query;
if (!q) {
res.send(`<h1>検索するものが指定されていません。</h1>`);
} else {
res.send(`<h1>「${q}」の検索結果</h1>`);
}
});
GETとPOSTのリクエストの違い
GET
- 情報の取得に使用
- データはクエリストリングで送られる(URLで見てわかる)
- 送られるデータの量に注意
POST
- データをサーバーに送信するときに使用
- 書き込み・作成・更新に使用する
- データはクエリストリングではなく、リクエストボディで送信する(URLを見てもわからない)
- どんな種類のデータでも送信可能(JSON)
フォームから送られてきた値を取得して使用する
express.urlencoded(): HTMLフォームから送信されるデータを解析
express.json(): JSON形式のデータを解析
これらのミドルウェアがないと req.body が undefined になってしまう。
フォームから送信されたデータは req.body に格納されます
分割代入で meat と qty を取り出して使用可能。
app.post("/tacos", (req, res) => {
const { meat, qty } = req.body;
res.send(`${meat}を${qty}個どうぞ`);
});
取得したreq.bodyの中身は下記
{
"meat": "meat",
"qty": "4"
}
const express = require("express");
const app = express();
// パースしてデータ形式を変更してあげる
app.use(express.urlencoded({ extended: true })); // フォームからだけ送られてきたデータをパースできる
app.use(express.json()); // JSONもパースする
// GETメソッド
app.get("/tacos", (req, res) => {
res.send("GET /tacos response");
});
// POSTメソッド
app.post("/tacos", (req, res) => {
const { meat, qty } = req.body; // 分割代入で取得、フォームから送られてきたデータをリクエストのボディからデータを取得する、パースできるとreq.bodyに入ってくる
console.log(req.body);
res.send(`${meat}を${qty}個どうぞ`);
});
// サーバー立ち上げ
app.listen(3000, () => {
console.log("ポート3000で待受中。。。");
});
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<h1>GETとPOSTリクエスト</h1>
<h2>GET</h2>
<form action="http://localhost:3000/tacos" method="get">
<input type="text" name="meat" placeholder="meat" />
<input type="number" name="qty" placeholder="個数" />
<button>Submit</button>
</form>
<hr />
<h2>POST</h2>
<form action="http://localhost:3000/tacos" method="post">
<input type="text" name="meat" placeholder="meat" />
<input type="number" name="qty" placeholder="個数" />
<button>Submit</button>
</form>
</body>
</html>
RESTについて
REPRESENTATIONAL
STATE
TRANSFER
のこと。
分散システムにおいて、複数のソフトウェアを連携させるのに適した設計原則の一つ。
クライアントとサーバーがどのようにコミュニケーションをとり、与えられたソースに対してどうCRUD操作を行うべきかと言うガイドライン・思想のこと。
※CRUDとは:データベース管理システム(DBMS)でデータを操作する際の4つの基本的な機能
Create(作成)、Read(読み出し)、Update(更新)、 Delete(削除)
※ルーティングとは:ネットワーク上でデータを転送する際に、最適な経路を選択するプロセスのこと。
REST fulは、RESTに則って作られたもののこと。
URLパターンやメソッドの使用ルールを決めてメンテナンスもしやすく、
チーム全体でのコミュニケーションを円滑にする共通のルールのように設定する。
REST fulなコメント管理アプリを作成してみる
REST fulなルーティングを作成する
/comments をベースとする、用途に合わせてhttpメソッドを変える
GET /comments - コメント一覧を取得
POST /comments - 新しいコメントを作成
GET /comments/:id - 特定のコメントを一つ取得
PATCH /comments/:id - 特定のコメントを更新
DELETE /comments/:id - 特定のコメントを削除
コメント詳細へのリンクを取得する際に重複しない一意の値(ID)が必要になってくるので、
UUIDを使用して新規コメントに対してもuuidを使用して一意のIDを付与する。
const express = require("express");
const app = express();
const { v4: uuid } = require("uuid");
uuid();
// // パースしてデータ形式を変更してあげる
app.use(express.urlencoded({ extended: true })); // フォームからだけ送られてきたデータをパースできる
app.use(express.json()); // JSONもパースする
app.set("view engine", "ejs");
// REST fulなルーティングを作成する
// /comments をベースとする、用途に合わせてhttpメソッドを変える
// GET /comments - コメント一覧を取得
// POST /comments - 新しいコメントを作成
// GET /comments/:id - 特定のコメントを一つ取得
// PATCH /comments/:id - 特定のコメントを更新
// DELETE /comments/:id - 特定のコメントを削除
const comments = [
{ id: uuid(), username: "Dave", comment: "Happy!!" },
{ id: uuid(), username: "鈴木", comment: "幸せですか?" },
{ id: uuid(), username: "田中", comment: "ワロス" },
{ id: uuid(), username: "さんちゃん", comment: "ミラクルワンダフル!!!" },
{ id: uuid(), username: "三日月", comment: "タンバリン" },
];
// コメントを取得
app.get("/comments", (req, res) => {
res.render("comments/index", { comments }); //viewsからのファイルパスが間違っていると表示されないので注意
});
// 新規コメントを作成するルーティング
app.get("/comments/new", (req, res) => {
res.render("comments/new");
});
// コメントをPOSTで /comments へ送信し、配列に格納する
app.post("/comments", (req, res) => {
const { username, comment } = req.body; // req.bodyからusernameとcommentだけを取得
comments.push({ username, comment, id: uuid() }); // 取得したusername,commentを配列commentsにpush、本来はデータベース
res.redirect("/comments"); // commentsにリダイレクトする、何も指定しなければ302リダイレクト
});
// コメントを取得して表示する
app.get("/comments/:id", (req, res) => {
const { id } = req.params;
const comment = comments.find((c) => c.id === id); // findで一つだけ取得する
res.render("comments/show", { comment });
});
// サーバー立ち上げ
app.listen(3000, () => {
console.log("ポート3000で待受中。。。");
});
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>comments一覧</title>
</head>
<body>
<h1>comments一覧</h1>
<ul>
<% for(let c of comments) { %>
<li>
<%= c.comment %> - <%= c.username %><a href="/comments/<%= c.id %>"
>詳細</a
>
</li>
<%} %>
</ul>
<a href="/comments/new">新規コメント作成</a>
</body>
</html>
MongoDBについて
そもそもなぜデータベースが必要なのか?
- 大量のデータを効率的に扱い、保存することができる
- データの挿入・照会・更新・並び替え・絞り込みを容易にするツールを提供してくれる
- データへのアクセスを制御するセキュリティ機能を有している
- スケールする(規模の拡大)
SQLデータベースとNoSQLデータベース
SQLデータベース
リレーショナルデータベース
データを挿入する前に、テーブルのスキーマをあらかじめ定義する
テーブルの型を作ってその通りにデータを入れていく
複数のテーブル間でリレーション(紐づける・関連づける)ことができる
- MySQLなど
NoSQLデータベース
SQLを使用しない
ドキュメント型・キーバリュー型・グラフ型,etc...
スキーマレスでデータ構造が柔軟なもの
- MongoDB
- CouchDB
- etc...
MongoDBを使用する理由
- Node.jsやExpressと一緒によく使用される
- MEAN,MERN
- JavaScriptと相性がいい
- 使用者も多い
MongoDBコマンド操作
検索などで比較演算子や論理演算子などが使える
// dogsのデータベースに追加する
db.dogs.insertOne({ name: "ポチ", age: 3, breed: "corgi", catFriendly: true });
// 複数追加
db.dogs.insert([
{ name: "ハチワレ", age: 14, breed: "shiba", catFriendly: false },
{ name: "ちいかわ", age: 5, breed: "chiikawa", catFriendly: true },
]);
// catsデータベースに追加する
db.cats.insert({ name: "tama", age: 6, dogFriendly: false, breed: "fold" });
// データを探す
db.dogs.find({ breed: "corgi" });
// データの更新
db.dogs.updateOne({ name: "ポチ" }, { $set: { age: 4 } });
// dogsデータベースの中でageが5より小さい
db.dogs.find({ age: { $lt: 5 } });
Mongooseとは?
アプリでデータを操作する際は、アプリとMongoDBを直接やりとりできるようにする。
それがDriver
MongooseはNode.js用のMongoDBのObject Data Mapping(ODM)ライブラリ
ODMとは?
Object Data Mapper
MongooseのようなODMはデータベースから送られてくるデータを、JavaScriptのオブジェクトにマッピングする。
Mongooseはアプリケーションのデータをモデル化して、スキーマを定義することができる。データの検証、複雑なクエリをJavaScriptで簡単に作成できる。
スキーマとは?
データベースにおける設計図・ルールのようなもの。
できること
- データ型の定義(データの一貫性を保つ)
- バリデーションルール設定
- デフォルト値の設定
何がいいのか?
- 予測可能な形式でデータが保存できる
- データ構造が明確になる
- チーム開発の共通理解が簡単になる
MongooseのCRUD処理
const mongoose = require("mongoose");
mongoose
.connect("mongodb://localhost:27017/movieApp", {
useNewUrlParser: true,
useUnifiedTopology: true,
})
.then(() => {
console.log("接続完了!");
})
.catch((error) => {
console.log("接続エラー!", error);
});
// スキーマを定義する(データベースにおける設計図・ルール)
const movieSchema = new mongoose.Schema({
title: String,
year: Number,
score: Number,
rating: String,
});
// モデル(クラス)の作成
const Movie = mongoose.model("Movie", movieSchema);
// インスタンスを作成
const amadeus = new Movie({
title: "Amadeus",
year: 1986,
score: 9,
rating: "R",
});
// Mongooseに複数データを追加する
// Movie.insertMany([
// { title: "GUNDAM", year: 1978, score: 7.7, rating: "R" },
// { title: "ZGUNDAM", year: 1989, score: 5, rating: "R" },
// { title: "SEED", year: 2001, score: 6, rating: "R" },
// { title: "SEED DESTINY", year: 2004, score: 10, rating: "R" },
// { title: "Witch from Mercury", year: 2023, score: 8, rating: "PG-13" },
// ]).then((data) => {
// console.log("成功!", data);
// })
// Expressアプリでめちゃくちゃよく使う、データの検索
Movie.findById("676120b1790d14239aadd818").then((m) => console.log(m));
// データの更新
Movie.updateOne({ title: "sample" }, { year: 2200 }).then((res) =>
console.log(res)
);
// 複数データの更新
Movie.updateMany(
{ title: { $in: ["sample", "GUNDAMSEED"] } },
{ score: 100 }
).then((res) => console.log(res));
// データの削除
Movie.deleteOne({ title: "sample" }).then((msg) => console.log(msg));
// yearが1999以上のものを全て削除
Movie.deleteMany({ year: { $gte: 1999 } }).then((msg) => console.log(msg));
バリデーションについて
設定したルールに適していない値を弾くことができる。
カスタムメッセージや、enumのように、入る値をコントロールすることもできる。
実行コマンドのメモ
// MongoDBサービスの起動
brew services start mongodb-community
// MongoDBシェルの起動
mongosh
// MongoDBシェルの終了
exit
// データベース一覧表示
show dbs
// きれいに整形して表示(コレクション名は、作成したモデルの複数形小文字)
db.コレクション名.find()
例)db.products.find()
// MongoDBサービスの停止
brew services stop mongodb-community
// 実行
node product.js
const mongoose = require("mongoose");
mongoose
.connect("mongodb://localhost:27017/shopApp", {
useNewUrlParser: true,
useUnifiedTopology: true,
})
.then(() => {
console.log("接続完了したよ!!");
})
.catch((error) => {
console.log("接続エラー!", error);
});
// MongooseスキーマをmongooseSchemaインスタンスとして作成
const productSchema = new mongoose.Schema({
name: {
type: String,
required: true,
},
price: {
type: Number,
required: true,
min: [0, "priceは0より大きい値にしてください"], // カスタムエラーメッセージを出す
},
onSale: {
type: Boolean,
default: false,
},
categories: [String],
qty: {
online: {
type: Number,
default: 0,
},
inStore: {
type: Number,
default: 0,
},
},
size: {
type: String,
enum: ["S", "M", "L"],
},
});
// コレクション名はproducts Product → products となる
// Mongooseモデルの作成
const Product = mongoose.model("Product", productSchema);
// Productモデルの新しいドキュメントインスタンスを作成
const bike = new Product({
name: "ジャージ",
price: 1900,
categories: ["サイクリング"],
size: "S",
});
bike
.save()
.then((data) => {
console.log("成功!", data);
})
.catch((err) => {
console.log("エラー!!", err);
});
モデルにメソッドを追加することができる(インスタンスメソッド)
// アロー関数ではなく通常のfunctionにする。理由はthisが定義した場所のthisになってしまうから、読んでくれたインスタンスがthisになるようにする
productSchema.methods.greet = function () {
console.log("ハロー!!");
console.log(`- ${this.name}からの呼び出しです。`); // greetを呼んだインスタンスが入ってくる
};
// モデルにメソッドを追加することができる
// onSaleの値を反転させるメソッド
productSchema.methods.toggleOnSale = function () {
this.onSale = !this.onSale;
return this.save();
};
// カテゴリーの追加メソッド
productSchema.methods.addCategory = function (newCat) {
this.categories.push(newCat);
return this.save();
};
const Product = mongoose.model("Product", productSchema);
const findProduct = async () => {
const foundProduct = await Product.findOne({ name: "安全なヘルメット" });
console.log(foundProduct);
await foundProduct.toggleOnSale(); // セール中かどうかの反転
console.log(foundProduct);
await foundProduct.addCategory("アウトドア"); // カテゴリーの追加
console.log(foundProduct);
};
findProduct();
モデルにメソッドを追加することができる(staticメソッド)
// staticメソッド、(モデル自身がthisとなる)全品セール、priceも0円
productSchema.statics.fireSale = function () {
return this.updateMany({}, { onSale: true, price: 0 });
};
// 呼び出し
Product.fireSale().then((msg) => console.log(msg));
インスタンスメソッドとstaticメソッドの違い・使い分け
インスタンスメソッド
- 個別のドキュメントに対する操作
- thisは呼んだインスタンスを指す
- 「特定の商品の値段を変更する」(その商品のみに影響)
productSchema.methods.toggleOnSale = function() {
this.onSale = !this.onSale;
return this.save();
};
// 使用例
productSchema.statics.fireSale = function() {
return this.updateMany({}, { onSale: true, price: 0 }); // 全商品が対象
};
productSchema.statics.sortByPrice = function() {
return this.find({}).sort({ price: 1 }); // 全商品を価格順に並び替え
};
// 使用例
await Product.fireSale(); // 全商品が0円のセール対象に
const sortedProducts = await Product.sortByPrice(); // 全商品を価格順に取得
staticメソッド
- コレクション全体に対する操作
- thisはクラスを指す
- 「お店全体の方針を変更する」(全商品に影響)
// 使用例
await Product.fireSale(); // 全商品が0円のセール対象に
const sortedProducts = await Product.sortByPrice(); // 全商品を価格順に取得
MongooseとExpressを同時に使用するチュートリアル
ミドルウェアを使用・自作する
ミドルウェアとは
「リクエストが来てから」「レスポンスを返すまで」の間の工程で、必要な処理を追加できる仕組み。
app.useについて
app.useは全てのリクエストに対して適用される。
- リクエストが来る
- app.useで登録された順番にミドルウェアが実行される
- 最後にルートハンドラが実行される
※ ルートハンドラとは
- HTTPリクエストを受け取って処理する関数
- リクエストの最終的な目的地
- レスポンスを生成して返す役割を持つ
// 基本的なルートハンドラの構造
app.get('/products', (req, res) => { // これがルートハンドラ
res.render('products/index');
});
使用例
// 全てのリクエストでログを出力
app.use(morgan('dev'));
// 特定のパスのみに適用するミドルウェア
app.use('/admin', adminMiddleware); // /admin/*のリクエストのみに適用
// 個別のルートに対するミドルウェア
app.get('/products', authMiddleware, (req, res) => {
// ...
}); // 全てのリクエストに対してログを出力
ミドルウェアを自作する
ミドルウェア関数の中でnext()を呼ぶことで次のミドルウェアに処理を渡す(Node.jsは本質的にミドルウェアの集合なので次のミドルウェアでOK)
最後のミドルウェアまで実行された後、レスポンスが返される。
ミドルウェアのバトンリレー的なもの。
// 自作したミドルウェア①
app.use((req, res, next) => {
console.log("自作したミドルウェア!!!");
return next(); // nextを呼ばないと処理が止まってレスポンスまで行かない!
});
// 自作したミドルウェア②
app.use((req, res, next) => {
console.log("2個目のミドルウェア!!!");
return next();
});
特定のルーティングに対してミドルウェアを組み込む
app.useで全体のルーティングへのミドルウェアの設定だけでなく、特定ルートのみのミドルウェアを設置することができる。
例)パスワード認証されていないと見れないページ(ルート)
※デモのため絶対に本番環境では下記のような実装はしない!セキュリティゆるゆる
// 簡易パスワード認証ミドルウェア(!本番では絶対やっちゃだめ!)
// 関数にすることで、特定のルーティングで使用することができる
const verifyPassword = (req, res, next) => {
const { password } = req.query;
if (password === "supersecret") {
return next();
}
res.send("パスワードが必要です。");
};
...
// 認証ページ
// 第二引数に作成したミドルウェアを設定して、特定のルーティングにだけミドルウェアを設定することができる
app.get("/secret", verifyPassword, (req, res) => {
res.send("ここは秘密のページ!!突破おめでとう!");
});
サーバーサイドのエラーハンドリング処理の追加
クライアントサイドのバリデーションだけだと、ブラウザからじゃなくPOST MANとかからリクエストが飛んでくるとエラーになってしまう。対策として、Expressの方でもエラーハンドリングの実装をする。
ミドルウェアを作成して、それを適用したいルーティングにコールバック関数として設定していく。(コールバック関数は何個でも設定できるため)
クライアントサイドとサーバーサイドの両方でバリデーション
- クライアントサイド:ユーザー体験の向上
- サーバーサイド:セキュリティの確保(Postmanなどの直接のAPIリクエストにも対応)
// 新規登録フォームの送信先のルーティング
// + validateCampground関数 → Joiでサーバーサイドのバリデーションをするミドルウェア
// + catchAsync関数 → 非同期処理でエラーがあったらエラーハンドリングミドルウェアに渡すミドルウェア
app.post(
"/campgrounds",
validateCampground,
catchAsync(async (req, res) => {
const campground = new Campground(req.body.campground);
await campground.save();
res.redirect(`/campgrounds/${campground._id}`); // 追加完了後は個別詳細ページにリダイレクト
})
);
# schema.js
const Joi = require("joi");
module.exports.campgroundSchema = Joi.object({
campground: Joi.object({
title: Joi.string().required(),
price: Joi.number().required().min(0),
image: Joi.string().required(),
location: Joi.string().required(),
description: Joi.string().required(),
}).required(),
});
# ExpressError.js
class ExpressError extends Error {
constructor(message, statusCode) {
super();
this.message = message;
this.statusCode = statusCode;
}
}
module.exports = ExpressError;
MongooseDBのリレーション
- 1対多数
- 1対超たくさん
などデータベースはリレーションしている。
const mongoose = require("mongoose");
// mongooseへの接続
mongoose
.connect("mongodb://localhost:27017/RelationDemo")
.then(() => {
console.log("MongoDB接続完了!");
})
.catch((error) => {
console.log("MongoDB接続エラー!", error);
});
// ユーザースキーマの定義
const userSchema = new mongoose.Schema({
first: String,
last: String,
addresses: [
{
country: String,
prefecture: String,
address1: String,
address2: String,
},
],
});
// クラスを作成
const User = mongoose.model("User", userSchema);
// インスタンスを作成
const makeUser = async () => {
const u = new User({
first: "taro",
last: "yamada",
});
u.addresses.push({
country: "日本",
prefecture: "北海道",
address1: "札幌市",
address2: "0番地",
});
const res = await u.save();
console.log("出力結果", res);
};
makeUser();
1対多のリレーションの基本的な作成パターン
ProductとFarmでそれぞれコレクションを作成してデータを保持。
Productコレクション
独立して商品データを保持
それぞれの商品は固有のObjectIDを持つ
Farmコレクション
牧場の基本情報を保持
products配列でProductのObjectIDを参照
これにより複数の商品とリレーションを持てる
リレーションの特徴
Farmは複数のProductを参照できる(1対多)
ProductはFarmを知らなくても存在できる(疎結合)
FarmのproductsにはProductのIDが格納される
farmのデータの形↓
{
_id: new ObjectId('6769228c7ca62546591d0636'),
name: 'まったり牧場',
city: '淡路市',
products: [
new ObjectId('6769208bbe5dfdd76a34d8f7'),
new ObjectId('6769208bbe5dfdd76a34d8f8')
],
__v: 1
}
const { Schema } = require("mongoose");
.
.
.
// farmスキーマを作成
const farmSchema = new Schema({
name: String,
city: String,
// ProductのIDを配列で保持
products: [{ type: Schema.Types.ObjectId, ref: "Product" }], // Productモデルを参照してIDが入る。
});
// クラスを作成
const Product = mongoose.model("Product", productSchema);
const Farm = mongoose.model("Farm", farmSchema);
// データの投入
// Product.insertMany([
// { name: "メロン", price: 498, season: "spring" },
// { name: "スイカ", price: 798, season: "summer" },
// { name: "ブドウ", price: 398, season: "fall" },
// { name: "ドラゴンフルーツ", price: 4980, season: "winter" },
// ]);
// 1. 新規作成時の関連付け
const makeFarm = async () => {
// 1. まず新しい牧場を作成
const farm = new Farm({ name: "まったり牧場", city: "淡路洲本市" });
// 2. データベースから関連付けたい商品(メロン)を検索
const melon = await Product.findOne({ name: "メロン" });
// 3. 商品を牧場のproducts配列に追加
// productsは配列として定義されているので、push()メソッドが使える
farm.products.push(melon);
// 4. 変更をデータベースに保存
await farm.save();
console.log(farm);
};
makeFarm();
populateの活用
populateとは
参照で保存されたIDを実際のデータに「展開」する機能で、必要な情報を効率的に取得できる強力なツール
IDを実際のデータに展開する機能という理解
データベースアクセスの効率化
なぜいいのか?
- データベースへのアクセスが1回で済む
- パフォーマンスの向上
- コードの可読性向上
// userスキーマの定義
const userSchema = new Schema({
username: String,
age: Number,
});
// tweetスキーマの定義
const tweetSchema = new Schema({
text: String,
likes: Number,
user: {
type: Schema.Types.ObjectId,
ref: "User",
},
});
// クラスを作成
const User = mongoose.model("User", userSchema);
const Tweet = mongoose.model("Tweet", tweetSchema);
const makeTweets = async () => {
await User.deleteMany({}); // 既存のデータを全消し
await Tweet.deleteMany({}); // Tweetも消しておくと良い
// ①新規userを作成
const user = new User({ username: "yamada", age: 20 });
// ②新規tweetを作成
const tweet1 = new Tweet({
text: "「吾輩は今日も縁側で昼寝をしながら、主人の苦々しい顔で論文を書いている様子を冷ややかに観察していた。書生は相変わらず無骨な様子で庭を掃いている。どうやら主人は近頃珍しく研究に没頭しているようだが、いかにも自惚れが強く見苦しい。その癖何も新しいことは書いていない。まったく人間というものは不可思議な生き物である。」",
likes: 0,
});
// ③tweet1のuserプロパティの中にuserを設定する
tweet1.user = user;
user.save();
tweet1.save();
};
// const makeTweets = async () => {
// // ①新規userを作成
// const user = await User.findOne({ age: 20 });
// // ②新規tweetを作成
// const tweet2 = new Tweet({
// text: "ホゲーーーーーーーーーーーーーーーーーー",
// likes: 100,
// });
// // ③tweet1のuserプロパティの中にuserを設定する
// tweet2.user = user;
// tweet2.save();
// };
// makeTweets();
const findTweet = async () => {
const t = await Tweet.find({}).populate("user", "username"); // populateで、userの中のusernameだけを取得する
console.log(t);
};
findTweet();
Mongooseのミドルウェアで削除
例)
・twitterでユーザーが削除されると投稿も全て削除される
・農場を削除すると、それに紐づく製品も全て削除される
関連データの削除をするにはミドルウェアを作成して自動処理をするのが一般的。
- データの一貫性の保証
- コードの重複を防ぐ
- 削除処理の忘れを防ぐ
// スキーマの下に自動処理のミドルウェアを作成
// farmが削除されるとその農場のproductsがあれば、すべて削除するミドルウェア
farmSchema.post("findOneAndDelete", async function (farm) {
if (farm.products.length) {
const res = await Product.deleteMany({ _id: { $in: farm.products } });
console.log(res);
}
});
// 削除ルーティング
app.delete("/farms/:id", async (req, res) => {
await Farm.findByIdAndDelete(req.params.id);
res.redirect("/farms");
});
<form action="/farms/<%= farm._id %>?_method=DELETE" method="post">
<button>農場を削除する</button>
</form>
ミドルウェアの中の関数を別の関数に切り出して管理
例)
キャンプ場が削除されるとそれに紐づくレビューも削除するミドルウェア
トリガー対象が異なってくる場合もあるので、別の関数に切り出して呼び出すことで、関心の分離ができる。
// キャンプ場が削除されると、reviewも削除する関数
const deleteFunc = async function (doc) {
if (doc) {
await Review.deleteMany({
_id: {
$in: doc.reviews,
},
});
}
};
// キャンプ場が削除されると、reviewも削除するミドルウェア
campgroundSchema.post("findOneAndDelete", deleteFunc);
// 異なるトリガーに同じロジックを簡単に適用できる
campgroundSchema.post("findOneAndDelete", deleteFunc);
campgroundSchema.post("findByIdAndDelete", deleteFunc);
campgroundSchema.post("remove", deleteFunc);
Expressのルーター
routesディレクトリを作成し、その中のファイルにルーターを定義し、呼び出して使用することができる。
routes
- shelters.js
index.js
const express = require("express");
const router = express.Router();
router.get("/", (req, res) => {
res.send("all shelters");
});
router.post("/", (req, res) => {
res.send("create shelter");
});
router.get("/:id", (req, res) => {
res.send("view shelter");
});
router.get("/:id/edit", (req, res) => {
res.send("edit shelter");
});
module.exports = router;
const express = require("express");
const app = express();
const shelterRoutes = require("./routes/shelters");
const dogsRoutes = require("./routes/dogs");
// プレフィックスのように使用でき、設定したRouterを使用することができる。
app.use("/shelters", shelterRoutes);
app.use("/dogs", dogsRoutes);
app.listen("3000", () => {
console.log("ポート3000で待機中");
});
routesフォルダにミドルウェアを設置する
router.use()を使うことで、特定のルートグループにのみ適用されるミドルウェアを設定できる。
例)adminルートにisAdminが入っているかどうかで表示を切り替えるミドルウェア
admin関連の認証ロジックを一箇所にまとめて管理できるようになる。便利。
const express = require("express");
const router = express.Router();
// router.でミドルウェアを作成することで、このページのrouterから始まるルート全てにミドルウェアを適用することができる
// adminに関するルート全体にミドルウェアを設定しまとめることができる
router.use((req, res, next) => {
if (req.query.isAdmin) {
return next();
}
res.send("Not Admin!");
});
router.get("/secret", (req, res) => {
res.send("secret!!!");
});
router.post("/deleteall", (req, res) => {
res.send("deleted all!!!");
});
module.exports = router;
app.use("/admin", adminRoutes);
cookieを学ぶことは認証処理の第一ステップ
そもそもcookieとは?
語源はフォーチュンクッキー?(クッキーの中に小さなメッセージが入っているお菓子)
特定のウェブサイトを閲覧する際に保存される小容量な情報のこと。
cookieが保存されるとブラウザはそのサイトへの次回以降のリクエストにそのクッキーを送信する。
cookieによって、HTTPをステートフル(状態を持たせる)にすることができる。
どんな用途で使用されているのか?
- セッション管理
- ログイン
- ショッピングカート
- ゲームのスコア...etc その他サーバーが覚えておくべきもの
- パーソナライゼーション
- ユーザー設定
- テーマ(darkモードとか)
- その他設定
- トラッキング
- ユーザーの行動の記録及び分析
Githubのcookieの使用例
Cookieの特徴
- サーバーから「渡される」もの
- ブラウザ側で「保存される」もの
- 次回アクセス時に「自動的に送信される」もの
例)cookieを持っているから、Youtubeに一度ログインすると、毎回ログインしなくていい。
cookieを渡す処理
// ブラウザにcookieを渡す設定
app.get("/setname", (req, res) => {
res.cookie("name", "yamada"); // キーと値で設定可能
res.cookie("animal", "cat");
res.send("cookie送ったよ");
});
リクエストからcookieを読み込む方法
npmコマンドでcookie-parserをインストールする。
const cookieParser = require("cookie-parser");
app.use(cookieParser());
app.get("/greet", (req, res) => {
const { name = "anonymous" } = req.cookies; // cookiesの中から、nameを取得する
res.send(`ようこそ${name}さん`);
});
署名付きcookie
データが改善されていないか?をチェックすることができる。
サーバーサイドから送ったcookieが、改竄されることなくそのままリクエストで返ってきていることを確認する技術。
※値を隠すための技術ではない!!
ユーザー認証やセッション管理において、データの信頼性を確保するための重要な機能
署名の付与
サーバーがCookieの値と秘密キーを使って署名を生成
値と署名をセットでCookieに保存
改ざん検知
Cookieが改ざんされると署名が無効になる
サーバーは署名を検証し、改ざんを検知できる
app.use(cookieParser("mysecret")); // 通常はこんな生の値ベタ打ちは絶対しない
// ブラウザに署名付cookieを渡す設定
app.get("/getsingedcookie", (req, res) => {
res.cookie("fruit", "grape", { signed: true });
res.send("署名付cookie");
});
// 署名付cookieはプロパティレベルで格納されている場所が違う
app.get("/verifyfruit", (req, res) => {
console.log(req.cookies); // 通常のcookie
console.log(req.signedCookies); // 署名付cookie
res.send(req.signedCookies);
});
セッションについて
セッションとは?
多くのデータをcookieでクライアントサイドで保存するのはあまり現実的ではない。そこでセッションが登場する。一時的な記憶装置的なもの。
HTTPをステートフルにするために使用するサーバー側のデータストア。
cookieを使用してデータを保存するのではなく、サーバー側にデータを保存する。
ブラウザにセッションを識別できるcookieを送信することで、サーバー側のセッションのデータを取得できるようにする。
cokieとの違い
- データを保存する場所
- cookie: ブラウザのクライアントさいど
- セッション: サーバー側
- 保存容量
- cookie: 容量制限が厳しい、大きなデータが保存できない(通常4KB程度)
- セッション: サーバーリソースの範囲で大量のデータを保存可能
セッションIDをルートに付与して訪問回数をカウントする
req.sessionはオブジェクトなので、coutなどの任意のプロパティを設定して管理をすることができる。
// 全てのルートにセッションが保存される
app.use(
session({
secret: "mysecret", // セッションIDの署名に使用する秘密鍵、通常は環境変数などを設定するので絶対にベタ打ちしない
})
);
// 何も設定していなければ、データストアは、MemoryStoreに保存される。開発・デバックの時だけ使用する
// 本番は、connect-redis,connect-mongoなどのデータストアを使用する
app.get("/viewcount", (req, res) => {
// セッションにcountが存在するかチェック
// req.session はオブジェクトなので、好きなプロパティ名を設定できる
if (req.session.count) {
req.session.count += 1; // セッションcountがあれば、+1していく、訪問するたびに+1されていく
} else {
req.session.count = 1; // セッションcountがなければ1にして初期化する。
}
res.send(`あなたは${req.session.count}回このページを表示しました。`);
});
フラッシュ
フラッシュ(Flash)メッセージは、一度だけ表示される一時的なメッセージのこと。
主にユーザーアクションの結果を通知する際に使用される。
例)
- 投稿の成功通知
- ログイン/ログアウト通知
- エラーメッセージ
- 警告メッセージ
農場の新規登録をすると、完了メッセージがflashで一度だけ出てくる処理
const session = require("express-session");
const flash = require("connect-flash");
const sessionOptions = {
secret: "mysecret",
resave: false,
saveUninitialized: false,
};
app.use(session(sessionOptions));
app.use(flash()); // 全てのリクエストオブジェクトでflashが使用できるようになる
// localsのmessagesにflashのsuccessを保存してどこでも呼び出せるようにするミドルウェア
app.use((req, res, next) => {
res.locals.messages = req.flash("success");
next();
});
// farmのform送信後のルーティング
app.post("/farms", async (req, res) => {
const farm = new Farm(req.body);
await farm.save();
req.flash("success", "登録に成功しました!"); // キーと表示したいテキスト
res.redirect("/farms");
});
フロントでの使用例
<!-- flashで1回だけ表示して消える -->
<%= messages %>
<h1>農場一覧ページ</h1>
routesディレクトリでRouteを定義する際の注意点
下記のような状態で、それぞれroutes/ ディレクトリ内で、
- campgrounds.js
- review.js
でrouteを定義するとき、review.jsの方で注意が必要。
注意点
デフォルトでは、各ルーターは独立したparamsオブジェクトを持つ
親ルート(/campgrounds/:id/...)のパラメータは子ルートに自動的には渡されない
mergeParams: trueを明示的に設定することで、親のパラメータを子ルートでも使用可能になる。
// 別ファイルで定義したRouteを使用する
app.use("/campgrounds", campgroundRoutes);
app.use("/campgrounds/:id/reviews", reviewRoutes);
下記のように、routerのoptionとして、mergeParamsとtrueに設定する必要がある(重要)
const express = require("express");
const router = express.Router({ mergeParams: true }); // mergeParamsで明示的に宣言する必要がある。親からIDが渡ってくるようになる
const catchAsync = require("../utils/catchAsync");
const ExpressError = require("../utils/ExpressError");
const Campground = require("../models/campground");
const Review = require("../models/review");
const { reviewSchema } = require("../schemas");
// reviewのバリデーションチェック自作ミドルウェア
const validateReview = (req, res, next) => {
const { error } = reviewSchema.validate(req.body);
if (error) {
const msg = error.details.map((detail) => detail.message).join(",");
throw new ExpressError(msg, 400);
} else {
next(); // 問題なければ次の処理に進む(必須!これがないと処理止まる)
}
};
// reviewの投稿追加のルーティング
// + Joiでサーバーサイドのバリデーションチェック
// + catchAsync関数 → 非同期処理でエラーがあったらエラーハンドリングミドルウェアに渡すミドルウェア
router.post(
"/",
validateReview,
catchAsync(async (req, res) => {
const campground = await Campground.findById(req.params.id);
const review = new Review(req.body.review);
campground.reviews.push(review);
await review.save();
await campground.save();
res.redirect(`/campgrounds/${campground._id}`);
})
);
// レビューの削除ルーティング
router.delete(
"/:reviewId",
catchAsync(async (req, res) => {
const { id, reviewId } = req.params;
// 1. キャンプ場のドキュメントから特定のレビューIDを取り除く
// $pullは配列から特定の要素を取り除くMongoDBの演算子
// reviews配列から reviewId と一致する要素を削除
await Campground.findByIdAndUpdate(id, { $pull: { reviews: reviewId } }); // campgroundの中で参照しているreviewsの中から、対象のreviewを取り除いてアップデートする
// 2. レビューコレクションから実際のレビュードキュメントを完全に削除する
await Review.findByIdAndDelete(reviewId); // 対象のレビュー自体を削除する
res.redirect(`/campgrounds/${id}`);
})
);
module.exports = router;
sessionの設定で大事なこと
httpOnly: true は重要なセキュリティ設定なので明示的に設定する
セッションCookieには常にhttpOnly: trueを設定すべき
httpOnlyの目的:
- JavaScriptからCookieへのアクセスを防ぐ
- クロスサイトスクリプティング(XSS)攻撃からの保護
// sessionの設定
const sessionConfig = {
secret: "mysecret",
resave: false,
saveUninitialized: true,
cookie: {
httpOnly: true,
// cookieの有効期限の設定
maxAge: 1000 * 60 * 60 * 24 * 7, // なんの数字かわかるように意図的に計算式にする。(1週間の有効期限)
},
};
app.use(session(sessionConfig));
エラーの場合のflashを作成して出し分ける
※注意点
successはデフォルトで空文字が入っていても表示されてしまうので、それを防ぐために、下記で条件分岐を作成する。
- successが存在するか確認(undefinedやnullを防ぐ)
- success.lengthで中身があるか確認(空配列やメッセージなしを防ぐ)
<!-- 成功の場合の処理 -->
<% if(success && success.length) { %>
<div class="alert alert-success alert-dismissible fade show" role="alert">
<%= success %><button
type="button"
class="btn-close"
data-bs-dismiss="alert"
aria-label="Close"
></button>
</div>
<% } %>
<!-- エラーの場合の処理 -->
<% if(error && error.length) { %>
<div class="alert alert-danger alert-dismissible fade show" role="alert">
<%= error %><button
type="button"
class="btn-close"
data-bs-dismiss="alert"
aria-label="Close"
></button>
</div>
<% } %>
<body class="d-flex flex-column vh-100">
<%- include('../partials/navbar.ejs') -%>
<main class="container mt-5">
<%- include('../partials/flash.ejs') -%><%- body -%>
</main>
<%- include('../partials/footer.ejs') -%>
// 詳細ページパスへのルーティング
router.get(
"/:id",
catchAsync(async (req, res) => {
const campground = await Campground.findById(req.params.id).populate(
"reviews"
);
// 削除された等でcampgroundがなかった時の処理&flashでエラーの文言を表示する
if (!campground) {
req.flash("error", "キャンプ場は見つかりませんでした。");
return res.redirect("/campgrounds");
}
res.render("campgrounds/show", { campground });
})
);
router.put(
"/:id",
validateCampground,
catchAsync(async (req, res) => {
const { id } = req.params;
// 分割代入でそれぞれに値が入る
const campground = await Campground.findByIdAndUpdate(id, {
...req.body.campground,
});
req.flash("success", "キャンプ場を更新しました!");
res.redirect(`/campgrounds/${campground._id}`); // 更新完了後は個別詳細ページにリダイレクト
})
);
認証の仕組みを理解する。
実際は認証はライブラリを使用することも多いかもしれないが、ゼロからコンセプトと処理の流れを理解することで使いこなせるようにする。仕組みを理解する。
認証(authentication)
ユーザーが「誰であるか」を確認するプロセス
多くの場合は、ユーザー名とパスワードの組み合わせで認証を行う。
セキュリティ質問や顔認証との組み合わせもある。
ログイン機能
認可(authorization)
ユーザーが、「何ができるか」を確認するプロセス。
一般的にはユーザーが認証された後に認可を行う。「あなたが誰なのかわかったので、何ができるか教えるね。」というイメージ。
何を見ていいのか?
何かを作成できるのか?編集することができるのか?削除できるのか?
コメントを投稿した本人なのか?
管理者権限の確認、権限の確認等
パスワードの扱い方について
パスワードはそのままデータベースに保存しない!
パスワードを使い回す人が多く、一つのサービスからパスワードが流出してしまうと、大損害になるかもしれない。
パスワードをハッシュ化する
パスワードをデータベースにそのまま保存するのではなく、パスワードをハッシュ関数にかけ、その結果をデータベースに保存する。
ハッシュ関数とは
- 任意のサイズの入力データを固定サイズの出力値に変換する関数。
- 一方向の変換(元のパスワードに戻せない)
- 同じ入力からは常に同じハッシュ値が生成される
- わずかな入力の違いで、全く異なるハッシュ値が生成される
- データベースの管理者でも元のパスワードは分からない
- ハッシュ値からパスワードを復元することは実質的に不可能
- ハッシュ化は一方向の処理なので復号できない
- サービス側は「パスワードが正しいかどうか」のみを確認できる
- 関数の実行が意図的に遅い
- 攻撃者:数百万回の試行に膨大な時間が必要
- 「時間をかけることによるセキュリティ」という考え方
パスワードの一致を確認する際は、
入力パスワード→ハッシュ関数→サーバーに保存されてあるハッシュ関数と比較して、一致していればログインができる
BCRYPT パスワードハッシュ化関数
パスワードの使い回しをする人も多いが、
よく使われるパスワードがある。
例)123456,qwerty,111111, etc...
もし万が一、世の中のハッシュ化関数がBCRYPTしかなかったら、
ハッシュ化された文字列→元のパスワードはできないが、
wikiとかに掲載されている、使用頻度高いパスワードをハッシュ化関数にかけて逆引き辞書のようなものを作成していたとしたら、万が一サーバーからパスワード漏れたときに、照合してパスワードがバレてしまう危険性がある。
そこで、SALTがある。
SALTとは
パスワードにランダムな任意の値をつけることで、ハッシュ関数の出力を大きく変えることができる。
一般的なセキュリティ攻撃を軽減するのに役立つ。
BCRYPT
bcryptは安全なパスワードハッシュ化のためのアルゴリズム
このような仕組みにより、bcryptは以下のような攻撃に対して強い耐性を持つ
- レインボーテーブル攻撃(事前計算された膨大なハッシュテーブルを使用した攻撃)
- ブルートフォース攻撃(総当たり攻撃)
- タイミング攻撃(処理時間の違いを利用した攻撃)
ランダムな文字列(ソルト)を生成
同じパスワードでも、異なるハッシュ値が生成される
const salt = await bcrypt.genSalt(saltRounds); // saltRounds = 12
ハッシュ化の計算回数を指定(2の12乗回)
数値が大きいほど計算時間が増加
ブルートフォース攻撃を困難にする
const hashPassword = async (pw) => {
const hash = await bcrypt.hash(pw, 12); // 12はコストファクター(ハッシュ化の計算回数を指定(2の12乗回))
console.log(hash);
};
比較処理
入力されたパスワードとハッシュ値を安全に比較
ハッシュに含まれているソルトを使用して比較を行う
// 入力されたパスワードと、ハッシュ化されたパスワードを比較して、一致すればログイン成功のメッセージがでる関数
// 比較する文字列(生パスワード)と、ハッシュ化された文字列
const login = async (pw, hashedPw) => {
const result = await bcrypt.compare(pw, hashedPw);
if (result) {
console.log("ログイン成功!");
} else {
console.log("ログイン失敗!");
}
};
ログイン機能を自作する
実際にはライブラリを使用することが多いが、仕組みとコンセプトを理解するために自作する。
重要ポイント
- ログイン認証のルーティングではGETは使用しない(パラメーターに生のパスワードが渡ってしまいセキュリティ的に危険なため)
- 渡ってきたパスワードは必ずハッシュ化して保存する
- セッションの秘密鍵は本番では必ず環境変数に登録する
- ユーザーIDをセッションに保存することで、「誰が」「ログインしているか?」を追跡できる
疑問
ユーザーIDでログイン状態を管理しているなら、
cookieを直接いじったらログイン状態をいじれるのではないか?
→できない
理由
- ユーザーIDそのものはcookieには保存されない
- セッションIDは署名付きで保存される
- 署名はsecretキーを使って生成される
- クライアント側でcookieを改ざんすると署名が無効になる
つまり、 - cookieを直接いじることはできても有効な署名付きセッションIDは作れない
- 改ざんされたcookieは認証に使えない
app.use(session({ secret: "mysecret" })); // 署名つきcookie,実際は環境変数として別ファイルで定義しておく
実装例
const bcrypt = require("bcrypt");
const session = require("express-session");
app.use(session({ secret: "mysecret" })); // 実際は環境変数として別ファイルで定義しておく
// ログイン認証とかのルーティングでGETは使用しない パラメーターに生のパスワードが渡ってしまって危険なため
app.post("/register", async (req, res) => {
const { username, password } = req.body;
const hash = await bcrypt.hash(password, 12); // req.bodyに入ってきたpasswordをハッシュ処理する
const user = new User({ username, password: hash });
await user.save();
// ここでユーザーIDを保存するのは、「誰なのか?」がわからないため。ログイン状態 + 誰なのか を保存する
req.session.user_id = user._id; // 登録が終わった後もセッションを保存
res.redirect("/");
});
// login用のルート
app.get("/login", (req, res) => {
res.render("login");
});
// login処理
app.post("/login", async (req, res) => {
const { username, password } = req.body;
const user = await User.findOne({ username });
const validPassword = await bcrypt.compare(password, user.password); // 入力されたパスワードと、ハッシュ化されたパスワードの比較を行う
if (validPassword) {
req.session.user_id = user._id; // セッションを保存
res.redirect("/secret"); // ログイン成功するとsecretルートにリダイレクト
} else {
res.redirect("/login"); // passwordが違うと、loginルートのリダイレクト
}
});
// loginしているときだけアクセス可能にする
app.get("/secret", (req, res) => {
if (!req.session.user_id) {
return res.redirect("/login");
}
res.send("ここはログイン済みの人だけが見れるページ!!!!");
});
ログアウト処理
機能的には、ログイン時にセッションに保存されたユーザー情報を破棄するだけでOK
// logoutルート
app.post("/logout", (req, res) => {
// req.session.user_id = null; // sessionの中に保存されているuser_idをnullにするだけ
req.session.destroy(); // sessionを破壊してしまう
res.redirect("/login");
});
ログイン状態の監視をミドルウェアに移す
// Login状態をチェックするミドルウェア
const requireLogin = (req, res, next) => {
if (!req.session.user_id) {
return res.redirect("/login");
}
next();
};
// requireLoginのミドルウェアを使用して、loginしているときだけアクセス可能にする
app.get("/secret", requireLogin, (req, res) => {
res.render("secret");
});
ログインチェックをモデルの中に移す
userSchema.statics.findAndValidate = async function (username, password) {
const foundUser = await this.findOne({ username });
const isValid = await bcrypt.compare(password, foundUser.password);
return isValid ? foundUser : false; // isValidがvalidならfoundUserを返す、無いならfalseを返す
};
// login処理
app.post("/login", async (req, res) => {
const { username, password } = req.body;
const foundUser = await User.findAndValidate(username, password);
if (foundUser) {
req.session.user_id = foundUser._id; // セッションを保存
res.redirect("/secret"); // ログイン成功するとsecretルートにリダイレクト
} else {
res.redirect("/login"); // passwordが違うと、loginルートのリダイレクト
}
});
モデルの中でパスワードハッシュ化のミドルウェアを作成する
// pre(save)で保存の処理の前にパスワードをハッシュ化するミドルウェア
userSchema.pre("save", async function (next) {
// isModified("password")は、パスワードフィールドが変更されたかをチェックしている、変更されていない場合は、nextで次の処理に移る
if (!this.isModified("password")) return next();
this.password = await bcrypt.hash(this.password, 12);
next();
});
// ログイン認証とかのルーティングでGETは使用しない パラメーターに生のパスワードが渡ってしまって危険なため
app.post("/register", async (req, res) => {
const { username, password } = req.body;
const user = new User({ username, password });
await user.save(); // この前に、モデルの方で定義したミドルウェアが走って、パスワードが保存されたときだけ、パスワードをハッシュ化する処理が走る
// ここでユーザーIDを保存するのは、「誰なのか?」がわからないため。ログイン状態 + 誰なのか を保存する
req.session.user_id = user._id; // 登録が終わった後もセッションを保存
res.redirect("/");
});
passportライブラリを使用した認証機能の実装
使用ライブラリ(今回はローカルを使用)
passportは認証方式の追加が容易のため使いやすく、拡張しやすいライブラリ
パッケージインストール
npm i passport passport-local passport-local-mongoose
userモデルの作成
const mongoose = require("mongoose");
const { Schema } = mongoose;
const passportLocalMongoose = require("passport-local-mongoose");
// ※ この時点でusername,passwordは定義しない
// passport-local-mongooseプラグインを使用すると、Schemaに自動的に以下のフィールドが追加される。
// 以下は明示的に書く必要がない(自動追加される)
// {
// username: String, // ユーザー名
// hash: String, // ハッシュ化されたパスワード
// salt: String // パスワードのソルト
// }
const userSchema = new Schema({
// emailはpassport-local-mongooseの対象外で、emailは認証には直接使用されないため、自動生成されないため明示的に書く
// パスワードリセット機能の実装、ユーザーへの通知メール送信...etc
email: {
type: String,
required: true,
unique: true,
},
});
// ここでpassportLocalMongooseのプラグインを使用、optionでメッセージの文言をカスタマイズすることが可能。
userSchema.plugin(passportLocalMongoose, {
errorMessages: {
MissingPasswordError: "パスワードが入力されていません",
AttemptTooSoonError:
"アカウントは現在ロックされています。しばらく時間をおいて再試行してください",
TooManyAttemptsError:
"ログイン試行回数が多すぎるため、アカウントがロックされました",
NoSaltValueStoredError: "認証できません。ソルト値が保存されていません",
IncorrectPasswordError: "パスワードまたはユーザー名が正しくありません",
IncorrectUsernameError: "パスワードまたはユーザー名が正しくありません",
MissingUsernameError: "ユーザー名が入力されていません",
UserExistsError: "このユーザー名は既に使用されています",
},
});
module.exports = mongoose.model("User", userSchema);
usersルートの作成
const express = require("express");
const router = express.Router(); // mergeParamsで明示的に宣言する必要がある。親からIDが渡ってくるようになる
const User = require("../models/users");
const passport = require("passport");
// GET /register 登録ページのルート
router.get("/register", (req, res) => {
res.render("users/register");
});
// POST /register 登録フォームのリクエスト先
router.post("/register", async (req, res) => {
try {
const { email, username, password } = req.body;
const user = await new User({ email, username });
// register()の裏で起こっていること: パスワードのソルト生成,パスワードのハッシュ化,ユーザー情報の保存,重複チェック
const registeredUser = await User.register(user, password);
console.log(registeredUser); // 生成データの確認用
req.flash("success", "yelp-campへようこそ!");
res.redirect("/campgrounds");
} catch (e) {
req.flash("error", e.message);
res.redirect("/register");
}
});
// loginルート
// GET /login ログインページのルート
router.get("/login", (req, res) => {
res.render("users/login");
});
// POST /login ログインフォームのリクエスト先
// passportのミドルウェアを差し込んで、optionを設定するだけで認証プロセスを実行してくれる
router.post(
"/login",
// authenticate()の裏で起こっていること:ユーザー検索,パスワード検証,セッション管理,エラーハンドリング
passport.authenticate("local", {
failureFlash: true, // 失敗時のフラッシュメッセージを有効化
failureRedirect: "/login", // 失敗時のリダイレクト先
}),
(req, res) => {
req.flash("success", "おかえりなさい!");
res.redirect("/campgrounds");
}
);
module.exports = router;
app.jsでの使用
const userRoutes = require("./routes/users");
// user,loginルート
app.use("/", userRoutes);
ログイン状態に応じて表示するボタンをだし分ける
<!-- ログイン状態に応じてレンダリングされるボタンを制御する -->
<div class="navbar-nav ms-auto">
<% if(!currentUser) { %>
<a href="/login" class="nav-link">ログイン</a>
<a href="/register" class="nav-link">ユーザー登録</a>
<% } else { %>
<a href="/logout" class="nav-link">ログアウト</a>
<% } %>
</div>
ログイン・ログアウトルート
// POST /login ログインフォームのリクエスト先
// passportのミドルウェアを差し込んで、optionを設定するだけで認証プロセスを実行してくれる
router.post(
"/login",
// authenticate()の裏で起こっていること:ユーザー検索,パスワード検証,セッション管理,エラーハンドリング
passport.authenticate("local", {
failureFlash: true, // 失敗時のフラッシュメッセージを有効化
failureRedirect: "/login", // 失敗時のリダイレクト先
}),
(req, res) => {
req.flash("success", "おかえりなさい!");
res.redirect("/campgrounds");
}
);
// ログアウトルート
router.get("/logout", (req, res) => {
req.logout((err) => {
if (err) {
// エラーがあった場合の処理
return next(err);
}
req.flash("success", "ログアウトしました!");
res.redirect("/campgrounds");
});
});
ログイン状態をチェックするミドルウェア
// ログイン状態をチェックするミドルウェア
module.exports.isLoggedIn = (req, res, next) => {
if (!req.isAuthenticated()) {
req.flash("error", "ログインしてください。");
return res.redirect("/login");
}
next();
};
app.use((req, res, next) => {
res.locals.currentUser = req.user; // localsのcurrentUserにreq.userを渡す
// res.localsに保存することで、そのリクエスト/レスポンスのサイクル中でテンプレート内のどこからでもアクセスできる
res.locals.success = req.flash("success"); // 成功した時のflash
res.locals.error = req.flash("error"); // エラーの時のflash
next();
});
安全なアプリケーションを作成するためのセキュリティ
フロントでボタンが見えてないから大丈夫。というわけではない。
URLを操作や、ブラウザの検証ツールを使用、POSTMANなどのツールを使用してデータを改竄しようとする人もいる。
多層防御の考え方が重要になる。
セキュリティは「見えないから大丈夫」ではなく、「アクセスできないから大丈夫」という考え方が基本。
- フロントエンド層(第一の防衛線)
- ユーザビリティのため。だがこれだけだと不十分。
// ログイン状態に応じたUI制御
<% if(currentUser && campground.author.equals(currentUser._id)) { %>
<button>編集</button>
<% } %>
- ルーティング層(重要な防衛線)
- 認証(Authentication): ユーザーが本人かどうか
- 認可(Authorization): そのリソースへのアクセス権があるか
// レビューの削除ルーティング
router.delete(
"/:reviewId",
isLoggedIn,
isReviewAuthor,
catchAsync(async (req, res) => {
const { id, reviewId } = req.params;
// 1. キャンプ場のドキュメントから特定のレビューIDを取り除く
// $pullは配列から特定の要素を取り除くMongoDBの演算子
// reviews配列から reviewId と一致する要素を削除
await Campground.findByIdAndUpdate(id, { $pull: { reviews: reviewId } }); // campgroundの中で参照しているreviewsの中から、対象のreviewを取り除いてアップデートする
// 2. レビューコレクションから実際のレビュードキュメントを完全に削除する
await Review.findByIdAndDelete(reviewId); // 対象のレビュー自体を削除する
req.flash("success", "レビューを削除しました!");
res.redirect(`/campgrounds/${id}`);
})
);
module.exports = router;
- データベース層(最後の防衛線)
- データの整合性を保証
- 不正なデータ操作を防ぐ
const campgroundSchema = new Schema({
author: {
type: Schema.Types.ObjectId,
ref: 'User',
required: true
}
});
画像のアップロード機能を実装する
MongoDBの中に直接画像を保管することはしない。
データベース自体に容量制限があるのと、スケールしづらい。
画像を保存するためのロジック
- フォームから画像をアップロード
- Cloudinaryへ画像を保管
- Cloudinaryから画像のURLが返ってくる
- 画像URLをMongoDB内に保存
外部サービスなどでAPIkeyを利用する際の注意点
APIkeyなどのAPIに関する情報は絶対に外部の人に漏らさない
有料サービスの場合は悪用されて金銭トラブルになりかねないので扱いに注意する。
ならどうすればいいのか?
環境変数を用意して格納してGithubなどにも公開しない。
.envファイルを用意し、その中に、keyと値のペアで格納していく。
- dotenvというパッケージを利用する。
npm i dotenv
ドキュメント見ながらcloudinaryをセッティングしていく。
例)
const cloudinary = require("cloudinary").v2;
const { CloudinaryStorage } = require("multer-storage-cloudinary");
cloudinary.config({
cloud_name: process.env.CLOUDINARY_CLOUD_NAME,
api_key: process.env.CLOUDINARY_KEY,
api_secret: process.env.CLOUDINARY_SECRET,
});
const storage = new CloudinaryStorage({
cloudinary: cloudinary,
params: {
folder: "YelpCamp",
allowed_formats: ["jpeg", "jpg", "png"],
},
});
module.exports = {
cloudinary,
storage,
};
const multer = require("multer");
const { storage } = require("../cloudinary");
const upload = multer({ storage }); // cloudinaryのstorageを保存先にする
router
.route("/")
.get(catchAsync(campgrounds.index))
// .post(
// isLoggedIn,
// validateCampground,
// catchAsync(campgrounds.createCampground)
// );
.post(upload.array("image"), (req, res) => {
console.log(req.body, req.files);
res.send("受付完了");
});
コンセプトと流れを理解してドキュメント見ながら実装する方が大事。
画像を扱う際の注意点・考慮すること
cloudinaryなどのAPIを利用して外部サービスに画像を保存する場合は、画像ファイルのアップロード枚数制限や、容量制限をかけるなどの対策が必要。従量課金制だと制限がない場合はいわゆるクラウド破産になりかねない。
セキュリティ対策
パフォーマンスの最適化
コスト管理
コンプライアンス対応
など、多角的な視点での対応が必要。
セキュリティ対策
クロスサイトスクリプティング(xss)
攻撃者が悪意のあるスクリプトをWebページに注入し、他のユーザーがそのページを閲覧した際にスクリプトが実行される攻撃です。これにより、セッションハイジャック、cookieの盗難、偽情報の表示など様々な被害が発生する可能性がある
対策
フロントエンドの対策
エスケープ処理: ユーザーからの入力をHTMLやJavaScriptとして出力する前に、特殊文字を適切にエスケープする。
サニタイズ: ユーザーからの入力を検証し、安全な文字列のみを許可する処理です。HTMLタグをすべて除去したり、特定のタグのみを許可したりすることができます。ただし、サニタイズは複雑な処理であり、不備があると攻撃を許してしまう可能性があるため、信頼できるライブラリ(例:DOMPurify)の使用が推奨される。
バックエンドの対策
入力の検証: フロントエンドでの対策に加えて、バックエンドでもユーザーからの入力を検証し、想定外のデータが送信されていないことを確認します。これは、攻撃者がフロントエンドのバリデーションをバイパスして、直接サーバーに悪意のあるデータを送信する可能性があるため。
インジェクションアタック(SQLインジェクション)
ユーザーからの入力が適切に検証・エスケープされずに、SQLクエリやOSコマンドなどに埋め込まれることで発生する攻撃です。これにより、データの不正取得、改ざん、削除などが発生する可能性がある
対策
フロントエンドの対策
入力の検証: フロントエンドでも、ユーザーからの入力が想定される形式(例:数値、メールアドレス、日付など)に合致しているか検証することで、ある程度の対策は可能です。しかし、根本的な対策はバックエンドで行う必要がある。
文字数制限: SQLインジェクションの際、文字数が多すぎる攻撃をある程度弾くことができる。
バックエンドの対策
プリペアドステートメント (パラメータ化されたクエリ): SQLクエリのプレースホルダーにパラメータをバインドすることで、ユーザー入力をデータとして安全に扱うことができます。SQLインジェクション対策の最も効果的な方法。
入力の検証: フロントエンドだけでなく、バックエンドでもユーザーからの入力を検証し、想定外のデータが送信されていないことを確認する。
XSS攻撃は実際にデモンストレーションでテストしてみて割と簡単にできてしまったため特に対策が必要と感じる、優先順位をつけてセキュリティ対策を講じ、セキュアなアプリケーションを作成することを目指す。できることから確実に。