N予備校「プログラミング入門Webアプリ」を受講しています。
今回は第3章20節〜26節です。
承認されたユーザーだけが使える匿名掲示板の作成。
気をつけたい箇所や気付いた点だけをまとめました。
設計の進め方
システム要件の定義
UIの設計: ページに何をどう配置するか
URIの設計: パスの作り方(RESTful)
モジュールの設計
URIとモジュール
リクエストを具体的な処理に振り分けることをルーティング、
リクエストに対し具体的な処理をする関数を リクエストハンドラという
サーバーを起動するもの
リクエストを処理するもの
ルーティングを行うもの
など、それぞれ役割を決めて実装する。
メインとなる機能から実装していくのが良い。
処理を実装する前にそれぞれの機能などをコメントする。
覚えておきたいこと
テンプレートエンジンなどを使う場合、のちにhtmlになるものはviewsディレクトリに格納する。
以下はPOSTされた時の処理
// POSTの処理
let body =[];
req.on('data', (chunk) => {
body.push(chunk);
}).on('end', ()=>{
// クエリから値を取得
body = Buffer.concat(body).toString();
const params = new URLSearchParams(body);
const content = params.get('content');
console.info(`投稿されました: ${content}`);
});
querystringが非推奨になったのでURLSearchParamを使う。URIエンコードされた文字列のデコードも同時に行うので、decodeURIComponentはいらない。
認証機能
http-authモジュールの、ファイルを利用した認証
users.htpasswd
admin:apple
guest1:1234
guest2:5678
const basic = auth.basic({
realm: 'Enter username and password.',
file: './users.htpasswd'
})
Basic認証では、特定のURLにアクセスした際、ステータスコード 401 - Unauthorized を返すことでログアウトされる。
function handleLogout(req, res){
handleStatus(req, res, 401, 'ログアウトしました');
}
function handleNotFound(req, res){
handleStatus(req, res, 404, 'ページが見つかりません');
}
function handleBadRequest(req, res){
handleStatus(req, res, 400, '未対応のメソッドです');
}
/**
* HTTPレスポンスの共通処理
* @param {Object} req HTTP Request
* @param {Object} res HTTP Response
* @param {Number} statusCode
* @param {String} msg Message
*/
function handleStatus(req, res, statusCode, msg){
res.writeHead(statusCode, {
'Content-Type' : 'text/plain; charset=utf-8'
});
res.end(msg);
}
データベース
今回はPostgreSQLをsequelizeモジュールで操作。
docker-compose.yml
services:
app:
depends_on:
- db
db:
image: postgres:12
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: secret_board
TZ: "Asia/Tokyo"
docker-compose.ymlにdbというサービス名でPostgreSQLのコンテナを追加。
仮想環境上でappとdbの二つのサーバーが稼働する状態。
Sequelize
yarn add sequelize@6.5.0
yarn add pg@8.5.1
yarn add pg-hstore@2.3.3
データベースの構成を設定することを、データモデリングという
post.js
// sequelizeの基本設定
const { Sequelize, DataTypes } = require('sequelize');
// データベース全体の設定
const sequelize = new Sequelize(
// IDとパスワードを渡す
'postgres://postgres:postgres@db/secret_board',
{
// 起動ログなどのログを出力しない
logging: false
}
);
// データモデリング
const Post = sequelize.define(
// テーブル名
'Post',
{
// データ定義
id: {
type: DataTypes.INTEGER,
autoIncrement: true,
primaryKey: true
},
content: {
type: DataTypes.TEXT
},
postedBy: {
type: DataTypes.STRING
},
trackingCookie: {
type: DataTypes.STRING
}
},
{
// テーブル名の固定
freezeTableName: true,
// createdAt, updatedAtを自動追加
timestamps: true
}
);
// このファイルの起動時にデータベースの設定を同期する
Post.sync();
// データモデリングしたオブジェクトをモジュールとして公開
module.exports = Post;
insert
const Post = require('./post');
Post.create({
//登録したいデータ
}).then(() => {
//登録後に実行してほしい処理
})
非同期処理となる(Promiseオブジェクトが返ってくる)ので、thenメソッドを利用する。
コンソール上でのデータ確認
dbコンテナへ入る
docker-compose exec db bash
su postgres : ユーザーの変更(今回はpostgresというユーザー)
psql : Postgres用コンソールに入る
¥c secret_borad : データベースに接続(今回はsecret_boardというデータベース)
¥q : 終了
select
データの全件取得
// データの全件取得
Post.findAll({order:[['id', 'DESC']]}).then((posts) => {
res.end(
pug.renderFile('./views/posts.pug', { posts })
);
});
renderFileの引数に変数を指定することで、pugで参照可能となる。
delete
destroyで指定したキーのデータを削除
投稿者自身か管理者のみが削除できるようにする
function handleDelete(req, res){
switch(req.method){
case 'POST':
let body = [];
req.on('data', (chunk) => {
body.push(chunk);
}).on('end', () =>{
body = Buffer.concat(body).toString();
const params = new URLSearchParams(body);
const id = params.get('id');
Post.findByPk(id).then((post) => {
if(req.user === post.postedBy || req.user === 'admin'){
post.destroy().then(() => {
handleRedirectPosts(req, res);
})
}
})
})
break;
default:
util.handleBadRequest(req, res);
break;
}
}
curlなどで第三者がdeleteを送る可能性があるため、サーバー側でもユーザーのチェックをすること。
実行するとデータベースから実際にデータを削除することを物理削除、実際にデータを削除する代わりに「削除された」ことを表すフラグを立ててデータが削除されたとみなすことを論理削除という。
削除処理は慎重に。実用的にも論理削除が良い。
データベースの永続化
dockerを破棄するとデータも破棄されるので、PC側の別の場所にファイルを保存する。
docker-compose.yml
db:
image: postgres:12
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: secret_board
TZ: "Asia/Tokyo"
volumes:
- ../secret-board-db:/var/lib/postgresql/data
PC側: ../secret-board-db
docker側: var/lib/postgresql/data
docker起動時に同期させる
トラッキングCookie
ユーザーの行動を追跡するために付与される Cookie のことをトラッキング Cookie と呼ぶ。
cookiesというnpmモジュールを利用する
yarn add cookies@0.8.0
const Cookies = require('cookies');
const trackingIdKey = 'tracking_id';
// TrackingCookie
const cookies = new Cookies(req, res);
addTrackingCookie(cookies);
function addTrackingCookie(cookies){
// Cookieが存在しない場合
if(!cookies.get(trackingIdKey)){
// ランダムな数値を登録
const trackingId = Math.floor(Math.random() * Number.MAX_SAFE_INTEGER);
// 有効期限を翌日(24時間後)まで
const tomorrow = new Date(Date.now() + (1000 * 60 * 60 * 24));
cookies.set(trackingIdKey, trackingId, {expires: tomorrow});
}
}
MAX_SAFE_INTEGER : JavaScriptで扱える最大値
PUG側
each post in posts
- let isPostedAdmin = (post.postedBy === 'admin')
if isPostedAdmin
h3 #{post.id} : 管理人 ★
else
h3 #{post.id} : ID:#{post.trackingCookie}
p!= post.content
p 投稿日時: #{post.createdAt}
- let isAdmin = (user === 'admin')
if isAdmin
p 投稿者: #{post.postedBy}
- let isDeletable = (user === post.postedBy || isAdmin)
if isDeletable
form(method="post" action="/posts?delete=1")
input(type="hidden" name="id" value=post.id)
button(type="submit") 削除
hr
each in
渡されてきたコレクションをループ。
-
pugテンプレート内にJavaScriptを直接記述することができる。
p!= post.content
pugテンプレート内で文字列に含まれたタグを認識させる。
まとめ
モジュールごとの実装、わかりにくかったのは名前のせいもあるのかな。
formからのPOSTメソッド、データベースのPost、データベースモデリングのpost.js、URIクエリのposts。
データベースからの削除は、なるほどと思った。
実際に削除するとなるとほかのテーブルも意識しないといけないので、実務的にも慎重になる事案。
しかし設計の重要性というのをつくづく感じる今日この頃。
↧