Soのブログ

技術、その他何でも

Next.js ver.15 ベストプラクティス

開発

ルーティング

  • app/配下のみに集約し、component/は同階層に分ける
app/
|  |-page.tsx
|  |...
|
components/
|
...

コンポーネント

  • なるべくデフォルトのServerComponentを使い、hooksを使う時はClientComponentを使う
  • ClientComponentの子は全てClientComponentとなるため、ClientComponentは末端に置くのがベスト

データフェッチ

  • Server Componentから非同期で同期的にデータを取得する場合は、データを取得後に画面が描画される。よってClient Componentを先に表示させ、Server ComponentはSuspenseでラップし、Loadingなどを表示させるのがベスト
<ClientComponent />
<Suspense fallback(<div>Loading...</div>)>
  <ServerComponent />
</Suspense>
  • まずはServerComponentを使うのがベスト
    • 初回読み込み早い
    • セキュア
    • キャッシュ利用など
  • リアルタイムはClientComponentを使う

キャッシュ

  • 実装箇所により使い分ける

レンダリング

  • ServerComponentからClientComponentは呼べるが逆は不可
  • ServerComponentでデータ取得がないとSSG、あるとSSR
  • 実装箇所により使い分ける
    • 静的→SSG、動的→SSR/CSR、中間→ISR

----------GPTで生成----------

  • ServerComponent・ClientComponentは実行場所、SSG・SSRCSR・ISRはレンダリング手法(いつどこでHTMLを作るか)
いつ どこで
SSG ビルド時 サーバ
SSR リクエスト時 サーバ
ISR ビルド時 サーバ
CSR ブラウザでJSが実行されたとき クライアント
  • ServerComponentでCSRは不可だが、ClientComponentでSSRなどは可
    • e.g. LikeButton だけが "use client" でClient Component、他はServer Componentとなり、HTMLはサーバで生成、JSでボタンだけ後から動く→ハイドレーション

----------GPTで生成----------

メタデータ

  • 基本設定を抑える
  • クローラー対策のため閉じた環境では対象外

ミドルウェア

  • URLリクエスト前に実行される関数のこと(認証とか)
  • 簡易的な認可チェックなどに使う

参考

Django REST Frameworkベストプラクティス

開発

Django 内容
同じ model, URLconf
違う serializer, view
なし form, template

認証はJWTを使う

  • トークン自体に認証情報を含み、サーバ側でログイン状態を管理する必要がないため、RESTの考え方と整合している
  • ブラウザのlocalStorageに保存する

参考文献

現場で使える Django REST Framework の教科書

JavaScriptベストプラクティス

開発

比較

  • 比較は===、!==を使う(==はあいまい比較となるため不可)
if (test1 === test2) {}

関数

  • 関数はアロー関数で書く
const test = () => {}

オブジェクト

  • オブジェクトはobj.keyで書く
const obj = {
  test: "test1"
}
obj.test
  • そのオブジェクトがどのようなプロパティ(オブジェクトのkey, valueのこと)を持っているかがわかりにくくなるため、作成後に新しいプロパティは追加しない

配列

  • 配列末尾へのアクセスは[-1]でなく[array.length - 1]を使う
array[array.length - 1]
  • 配列は非破壊的なメソッドを使う

this

  • thisは状況により参照先が変わるためメソッドではない通常の関数においてはthisは使わない
  • メソッドのthisはアロー関数のみで使い、スコープチェーンと同様に外側のスコープを探す

非同期処理

  • Async Function内でawait式を使って処理を待っている間も、関数の外側では通常どおり処理が進む。つまり、awaitで待つのはAsync Function内のみ
const test = async () => {
  await new Promise((resolve) => {
    setTimeout(resolve, 10)
  )}
}

console.log("1番目に実行")

test().then(() => {
  console.log("3番目に実行")
})

console.log("2番目に実行")
  • コールバック関数としてAsync Functionを渡す場合は意図しない挙動になる可能性があるため注意

JSON

// JSONはダブルクォートのみを許容するため、シングルクォートでJSON文字列を記述
const json = '{"id": 1, "name": "bob"}'
const obj = JSON.parse(json)
const obj = {id: 1, name: "bob"}
const json = JSON.stringify(obj)

Date

参考文献

Djangoベストプラクティス

開発

設定ディレクトリ名はconfigとする

django-admin startproject config .

staticとtemplatesはプロジェクト直下に

project
|--manage.py
|--config/
|--app/
|--static/
|--templates/

viewは基本のクラスベースで書き、徐々にリファクタリングする

class LoginView(View):
  def get(self, request, *args, **kwargs):
    context = {"form": LoginForm()}
    return TemplateResponse(request, "accounts/login.html", context)

login = LoginView.as_view()

viewには複雑なロジックは書かない

  • fat viewは避けたいので、なるべく一つのテーブルの処理に関しては、modelに記述するのが良い

Userモデルの拡張

# models.py
class CustomUser(AbstractUser):
  xx
# settings.py
# appはアプリ名
AUTH_USER_MODEL = "app.CustomUser"
  • get_user_model()でモデルを取得
# Userモデルの取得
get_user_model().objects.all()

コンテナで動かす時は0.0.0.0.を指定する

  • 0.0.0.0は全てのIPアドレスの範囲
  • ホストOSのブラウザからゲストOS(コンテナ)への接続時にlocalhostは不可
python manage.py runserver 0.0.0.0:8000

大量のオブジェクトを作る時はbulk_createを使う

user_objects = []
for _ in range(100000):
    user_objects.append(User(name="jobs"))

# user_objects のデータをDBに一括登録する
User.objects.bulk_create(user_objects)

本番

静的ファイル(リクエストに応じて中身を変更せずにそのまま返す)はWebサーバ(Nginxなどのリバースプロキシ)に置く

  • アプリケーションサーバDjango)は処理が必要なリクエストのみに集中することでパフォーマンスを上げる
  • セキュリティの観点より静的ファイルの配信元(STATIC_ROOT)とアプリケーション側の格納場所(STATICFILES_DIRS)は分ける
# STATICFILES_DIRSからSTATIC_ROOTへ静的ファイルをコピー
python manage.py collectstatic

メディアファイルも同様で、MEDIA_ROOTにS3などのディレクトリを指定し、Webサーバ側で処理する

settings.pyはbase.pyに共通部分をそれ以外に差分を書く

config/
|--settings/
  |--base.py
  |--local.py
  |--production.py
  |--test.py
# local.py
from .base import *
python manage.py runserver --settings config.settings.local.py

# モデルの差分をチェックしてマイグレーションファイルを作成
python manage.py makemigrations --settings config.settings.local

# マイグレーションファイルの内容をDBに反映してテーブルを作成・更新
python manage.py migrate

参考文献

RDBコンテナとDjangoコンテナのシークレット変数の設定

はじめに

RDB(Postgresなど)コンテナとDjangoコンテナのシークレット変数の設定を整理しました。

docker-composeで組むことが多いのですが、いつも迷うのでdocker-composeの場合で整理しました。

シークレット変数は環境変数又はファイルから読み込みますが、ファイルから読み込む形にします。

開発環境を想定します。

設定方法

.envにシークレット変数を記述する

# PostgreSQL初期化用(docker-compose.ymlで使用)
POSTGRES_USER=xxx
POSTGRES_PASSWORD=xxx
POSTGRES_DB=xxx

# Django設定用(settings.pyで使用)
DATABASE_URL=postgres://<user>:<password>@db:5432/<db>
SECRET_KEY=xxx

※<user>などは${POSTGRES_USER}のように展開不可のためハードコードするか、settings.pyでPOSTGRES_USERなどを読み込み、DATABASE_URLを構築する必要がある

RDBはコンテナ起動時にroot userやDBを作成する必要があるためdocker-compose.ymlから読み込む

services:
  db:
    environment:
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      POSTGRES_DB: ${POSTGRES_DB}

Djangoはsettings.pyから読み込む(docker-compose.ymlにenvironmentの記述不要)

import environ

env = environ.Env()
environ.Env.read_env(env_file=str(BASE_DIR) + "/.env")

# シークレット変数の読み込み
DATABASES = {
    # DATABASE_URLを読み込む
    "default": env.db(),
}
SECRET_KEY = env("SECRET_KEY")

React Todo List

アプリ開発時の気付き事項を記す。

github.com

アプリ開発

  • JSXでは全ての要素は閉じる
<input type="text" />
  • formに含まれるボタンが押されたとき、デフォルトでform全体が送信されるため、ページのリロードが発生する。そのため、追加されたToDoがすぐに消えてしまう。よってformタグは不要
  • li 要素に key を追加することで、Reactがどのアイテムが追加、削除、または変更されたかを効率的に追跡でき、警告も消える
<ul>
  {todos.map((todo, index) => (
    <li key={index}>
      <p>{todo}</p>
    </li>
  ))}
</ul>
  • Stateの更新の際のset~の更新対象となる変数が配列のときは新しい配列を渡して更新する
const [todos, setTodos] = useState([]);

const newTodos = [...todos, text];
setTodos(newTodos);
  • 三項演算子の中でも複数要素を返す場合は<>が必要
  • JavaScriptではオブジェクト内で同じキーを複数回使用すると、最後に記述された値で以前の値が上書きされる
const todo = {
  text: updateText,
  completed: false,
  updated: false,
  updated: true,
}

JavaScript カレンダーアプリなど

カレンダーアプリなど作成時の気付きを記す。

github.com

github.com

  • エントリーポイントとは、アプリケーションの中で一番最初に呼び出される部分のこと
  • Fetch APIはHTTP通信を行ってリソースを取得するためのAPI
  • fetch
    • Promiseを返す
    • thenメソッドが使える(Promiseを返すから)
    • jsonメソッドが使える
// HTTPリクエスト
const userId = "任意のGitHubユーザーID";
fetch(`https://api.github.com/users/${encodeURIComponent(userId)}`);
// HTTPレスポンス
const userId = "js-primer-example";
fetch(`https://api.github.com/users/${encodeURIComponent(userId)}`)
    .then(response => {
        console.log(response.status); // => 200
        return response.json().then(userInfo => {
            // JSONパースされたオブジェクトが渡される
            console.log(userInfo); // => {...}
        });
    });
// commanderモジュールからprogramオブジェクトをインポートする
import { program } from "commander";

// コマンドライン引数をcommanderでパースする
program.parse(process.argv);

// ファイルパスをprogram.args配列から取り出す
const filePath = program.args[0];
  • Node.jsでファイルの読み書きを行うには、標準モジュールのfsモジュールを使う
  • Node.jsの標準モジュールはnode:fsのようにnode:プリフィックスをつけてインポートできる。 プリフィックスを付けないfsでもインポートできるが、npmからインストールしたサードパーティ製のモジュールとの区別が明確になるため、付けておくことが推奨
// fs/promisesモジュール全体を読み込む
import * as fs from "node:fs/promises";

// fs/promisesモジュールからreadFile関数を読み込む
import { readFile } from "node:fs/promises";
  • ファイルの読み書きは存在の有無や権限、ファイルシステムの違いなどによって例外が発生しやすいので、必ずエラーハンドリング処理を書く
  • markedパッケージを使ってMarkdown文字列をHTML文字列に変換可能
  • テスティングフレームワークのmochaパッケージをインストールし、npm testコマンドでmochaコマンドを実行することによりテストが可能
$ npm test
> mocha test/
  • event.preventDefaultメソッドは、submitイベントの発生元であるフォームが持つデフォルトの動作をキャンセルする
    • フォームが持つデフォルトの動作とは、フォームの内容を指定したURLへ送信するという動作
    • form要素に送信先が指定されていないため、現在のURLに対してフォームの内容を送信
    • 現在のURLに対してフォームの送信が行われると、結果的にページがリロードされる
    • リロードさせないためにevent.preventDefaultメソッドを使う
formElement.addEventListener("submit", (event) => {
    // submitイベントの本来の動作を止める
    event.preventDefault();
    console.log(`入力欄の値: ${inputElement.value}`);
});
  • イベントが発生したことを元に処理を進める方法をイベント駆動(イベントドリブン)
  • commanderでコマンドライン引数にオプションを付ける
// コマンドライン引数の取得とオプションの設定
import {program} from 'commander';
program.option('-m, --month <number>');
program.parse(process.argv);
const options = program.opts();
console.log(options.month;);
  • 異常終了(0は正常終了)
process.exit(1);
  • 改行せずに表示
process.stdout.write('xxx');
  • 数字の二桁表示
// iは数字
i = i.toString().padStart(2, '0');
  • 複数の空白の作り方
// 空白数を返す
let fn_blank = (num) => {
  return '   '.repeat(num);
}
  • Webブラウザでモジュールを使用するためには、scriptタグにtype="module"属性を追加
<body>
  <h1>ToDo List</h1>
  <script type="module" src="./index.js"></script>
</body>
  • プライベートフィールドは#を付ける
// TodoListModelはAppクラスの外からは触る必要がないためプライベートフィールドとする
export class App {
  #todoListModel = new TodoListModel();
}
  • confirmで削除前のメッセージを表示させる
// OKならtrue、キャンセルならfalseを返す
let result = confirm('削除OK?')

参考文献