AI can fly !!

AI がやりたい Web エンジニアのアウトプット (AI の知識は無い)

【Python】pyenv で複数バージョンを管理する (チートシート付)【Ubuntu】

python-logo

はじめに

サードパーティ製のパッケージだけでなく、プログラミング言語自体も日々のアップデートによって頻繁にバージョンアップが行われており、継続的なアプリ開発を行っていく上では、アプリ毎に使用する言語やパッケージのバージョンを管理する必要が出てきます。

最近、 WSL 2 を使用して UbuntuPython の開発環境を構築した際に、複数の Python のバージョンを管理するために pyenv を導入したので、インストール方法と使い方についてまとめてみました。

おまけとして、主に使用する pyenv コマンドをチートシートとして合わせてまとめています。

動作環境

OS Version
Ubuntu 20.04 LTS

Windows 10 Pro の Windows Subsystem for Linux (WSL 2) を使用

Application Version
Git 2.25.1
pyenv 1.2.21

pyenv のインストール

pyenv のソースコードGitHub から clone して .bashrc へ必要な情報を追記するだけで、 pyenv のインストールは完了します。

詳細は GitHub の pyenv 公式の README.md に書いてあるので、こちらを見た方が間違いないです。

github.com

以下は pyenv 公式のインストール手順を参考に、実際に Ubuntu の環境へインストールした手順になります。

1. pyenv のソースコードを clone する

git clone https://github.com/pyenv/pyenv.git ~/.pyenv

ユーザのホームディレクトリ直下に、 GitHubリポジトリから pyenv のソースコードを clone します。

2. 環境変数を定義する

echo 'export PYENV_ROOT="$HOME/.pyenv"' >> ~/.bashrc
echo 'export PATH="$PYENV_ROOT/bin:$PATH"' >> ~/.bashrc

環境変数 PYENV_ROOT へ先程 clone した .pyenv ディレクトリのパスを定義し、次に環境変数 PATH.pyenv/bin を追加する処理を、ユーザのホームディレクトリ直下にある .bashrc へ追記します。

3. pyenv 初期化処理を .bashrc へ追記する

echo -e 'if command -v pyenv 1>/dev/null 2>&1; then\n  eval "$(pyenv init -)"\nfi' >> ~/.bashrc

pyenv init - では、 pyenv に必要な環境変数の定義や初期化処理が実行されます。

実際の処理の内容は、 echo "$(pyenv init -)" で確認できます。

4. Bash (シェル) を再起動する

exec "$SHELL"

.bashrc に必要な処理をすべて書いているので、 source ~/.bashrc でも大丈夫のような気がしますが、 pyenv 公式のインストール手順には上記スクリプトが書いてあるので、素直に Bash を再起動します。

正確には、新たなシェルを起動して自分自身と置き換える、でしょうか?

これで pyenv のインストールは完了です。

pyenv の使い方

pyenv コマンドを使用して、 Python のインストールや python コマンドで実行される Python のバージョンの設定を行います。

Python のインストール

# Python のバージョン 3.9.0 をインストール
pyenv install 3.9.0

インストールしたい Python のバージョンを指定して、 pyenv install コマンドを実行します。

pyenv でインストール可能な Python のバージョンは、 pyenv install --list で確認することができます。

【エラー発生時】Ubuntu に不足するパッケージを追加

pyenv で Python をインストールする際、パッケージが不足していると Python のビルドでエラーが発生するか、インストールに成功しても一部のモジュールが使用できない場合があります。

今回使用した環境は WSL 2 上のまっさらな Ubuntu 20.04 LTS ですが、 pyenv のみインストールして Python 3.9.0 をインストールしたところ、ビルドエラーが発生しました。

pyenv の Python のインストールログは、 /tmp/ ディレクトリに python-build.yyyymmddhh24miss.fff.log というファイル名で出力されます。

GitHubpyenv 公式 Wiki に OS 毎の推奨ビルド環境が記載されているので、こちらを参考に必要なパッケージを追加しましょう。

github.com

僕の環境 (Ubuntu 20.04 LTS) では、以下のパッケージを追加することで、正常に Python をインストールできるようになりました。

sudo apt update
sudo apt upgrade
sudo apt install build-essential libssl-dev zlib1g-dev libbz2-dev libreadline-dev libsqlite3-dev tk-dev libffi-dev liblzma-dev libgdbm-dev libdb-dev
Packages Description
build-essential gcc や make などのビルドパッケージ群
libssl-dev Python 標準モジュールの依存パッケージ
zlib1g-dev Python 標準モジュールの依存パッケージ
libbz2-dev Python 標準モジュールの依存パッケージ
libreadline-dev Python 標準モジュールの依存パッケージ
libsqlite3-dev Python 標準モジュールの依存パッケージ
tk-dev Python 標準モジュールの依存パッケージ
libffi-dev Python 標準モジュールの依存パッケージ
liblzma-dev Python 標準モジュールの依存パッケージ
libgdbm-dev Python 標準モジュールの依存パッケージ
libdb-dev Python 標準モジュールの依存パッケージ

Python のインストールは、正常に完了するまで何度でもリトライすることが可能です。

使用する Python のバージョン切替

# 現在のシェル固有の Python のバージョンを 3.9.0 に設定
pyenv shell 3.9.0

# カレントディレクトリ以下で固有の Python のバージョンを 3.9.0 に設定
pyenv local 3.9.0

# すべてのシェルで使用される Python のバージョンを 3.9.0 に設定
pyenv global 3.9.0

使用するコマンドにより、設定する Python のバージョンの有効範囲が変わります。

複数の設定が存在する場合の優先順位は shell > local > global となります。

現在設定されている Python のバージョンは、 pyenv version コマンドで確認できます。

Python のアンインストール

# Python のバージョン 3.9.0 をアンインストール
pyenv uninstall 3.9.0

不要になった Python のバージョンは、 pyenv uninstall コマンドでアンインストールできます。

venv との組み合わせ

pyenv は Python 自体のバージョンを管理するものですが、これと合わせて Python 標準ライブラリの venv を利用することで、設定した Python のバージョンでパッケージを管理する仮想環境を作成することができます。

# Python のバージョンを設定
pyenv shell 3.9.0

# カレントディレクトリに仮想環境を作成
python -m venv --upgrade-deps venv

# 仮想環境を有効化
source venv/bin/activate

この例では pyenv shell コマンドを使用して現在のシェル固有の Python のバージョンを設定していますが、次回からは pyenv での Python の設定内容にかかわらず、仮想環境を有効化するだけで仮想環境作成時に設定したバージョンの Python が使用できます。

pyenv コマンドチートシート

Command Description
pyenv install --list pyenv でインストール可能な Python のバージョンを一覧表示
pyenv install [version] 指定したバージョンの Python をインストール
pyenv versions pyenv でインストールした Python を一覧表示
pyenv shell [version] 現在のシェルで使用する Python のバージョンを設定
pyenv local [version] カレントディレクトリ以下で使用する Python のバージョンを設定
pyenv global [version] すべてのシェルで使用する Python のバージョンを設定
pyenv version 現在設定されている Python のバージョンを表示
pyenv uninstall [version] 指定したバージョンの Python をアンインストール

※ その他のコマンドや詳細は GitHub の pyenv 公式 Command Reference を参照

github.com

おわりに

pyenv を使用すれば一つの OS 上で複数の Python のバージョンを管理することができますが、アプリ開発環境という視点で見れば PythonPython パッケージのバージョンだけでなく、 Web サーバや DBMS などのミドルウェアのバージョンも管理する必要があります。

最近は WSL 2 によって Windows ユーザにも Docker が身近になりつつあるので、 今後は Web サーバや DBMS などのミドルウェアも合わせてコンテナ化し、一つのアプリ開発環境として管理するのが主流になるかもしれません。

とは言っても、手軽に Python のバージョン管理を行う際はやはり pyenv が有用なので、要件に合わせて使い分けするのがベターではないでしょうか。

ちなみに僕は今 Docker に興味津々なので、ある程度使えるようになったら、次は Docker のまとめエントリを書きたいと思っています (o´艸`)

【Oracle Cloud】Autonomous Transaction Processing への接続方法まとめ【Free Tier】

ORACLE CLOUD

はじめに

プライベートで使用している Oracle Database 19c から、 Oracle Cloud Infrastructure の Autonomous Database へデータベースの移行を行いました。

移行先は Autonomous Database のトランザクション処理に最適化された Autonomous Transaction Processing です。

Autonomous Transaction Processing をプロビジョニングしてデータベースを作成するまでは、 Web ブラウザから GUI ですべての操作を完了することができるので、特に難しいことはありません。

Oracle Data Pump を使用したデータ移行の方法は 別記事 を参照いただくとして、今回はクライアントから Autonomous Transaction Processing への接続方法についてまとめました。

ai-can-fly.hateblo.jp

ちなみに、 Oracle Cloud には Free Tier という常時無償のプランがあり、今回取り上げている Autonomous Transaction Processing の Always Free Autonomous Database では、一つのデータベースあたり 20GB まで Exadata ストレージを永久に無料で使用することができます。

動作環境

Cloud

Cloud
Oracle Cloud Infrastructure
Product
Oracle Autonomous Transaction Processing

Autonomous Transaction Processing とは

ハードウェアの構成や管理、ソフトウェアのインストールなどのデータベース管理を必要とせず、ワークロード要件に応じて自動スケーリングなどを行う Oracle Cloud 上に構築されたクラウド・データベース・サービスです。

OLTP (オンライントランザクション処理) およびリアルタイム分析アプリケーションのデータベース操作に最適化され、ミッションクリティカルなアプリケーションのリクエストを満たすために即座にスケーリングできる自動運転、自動保護、自己修復データベースサービスを提供します。

www.oracle.com

簡単に言えば、運用・保守などをほとんど考えずに使用できる、めちゃくちゃ使い勝手の良い Oracle Database です!

Client

OS Version
Windows 10 Pro 1909
Application Version
PowerShell 5.1.18362.752
SQL*Plus (Oracle Client) Release 19.0.0.0.0 - Production

前提条件

  • Oracle Cloud Infrastructure アカウントを作成済み
  • Oracle Autonomous Transaction Processing インスタンスをプロビジョニング済み
  • クライアント PC に Oracle Client をインストール済み

Autonomous Transaction Processing への接続

Autonomous Transaction Processingは Oracle Net Services をサポートしており、インターネットを介して複数の方法で接続することができます。

今回はクライアント資格証明 (ウォレット) を使用して、 Autonomous Transaction Processing へ Oracle Call Interface (OCI) 接続を行う方法を取り上げます。

接続準備

クライアント資格証明 (ウォレット) のダウンロード

まず、クライアント資格証明 (ウォレット) を Oracle Cloud Infrastructure コンソール から ダウンロードします。

ダウンロードの詳細は、 Autonomous Transaction Processing の公式ドキュメントをご覧ください。

docs.oracle.com

ダウンロードしたクライアント資格証明 (ウォレット) は Zip 形式に圧縮されていますが、 Oracle Call Interface 接続で使用する際は解凍する必要はないので、Zip 形式のまま任意のディレクトリに格納します。

格納場所に指定はありませんが、接続情報が記載されたファイルなのでセキュアなディレクトリに保管しましょう。

今回は例として、ユーザのホームディレクトリの直下へ専用のディレクトリを作成して格納します。

C:\Users\hrgm\OracleCloud\Wallet.zip

なお、クライアント資格証明 (ウォレット) は Zip 形式のまま使用しますが、後述する sqlnet.oratnsnames.ora の編集を行う際に圧縮ファイルの中のファイルを参照する必要があるため、どこでもいいので一時的なワーキングディレクトリに解凍もしておきます。

sqlnet.ora (プロファイル構成ファイル) の更新

解凍したクライアント資格証明 (ウォレット) の中にある sqlnet.oraテキストエディタで開き、その中の 1 行をクライアント PC にインストールされている Oracle Client の sqlnet.ora へ追記します。

[クライアント資格証明 (ウォレット)]/sqlnet.ora

WALLET_LOCATION = (SOURCE = (METHOD = file) (METHOD_DATA = (DIRECTORY="?/network/admin")))
SSL_SERVER_DN_MATCH=yes

sqlnet.oraWALLET_LOCATION = ... の 1 行をコピーし、クライアント PC の sqlnet.ora の最終行にペーストします。

ペースト後、 DIRECTORY の値を Zip 形式のクライアント資格証明 (ウォレット) を格納したディレクトリパスとクライアント資格証明 (ウォレット) ファイル名に修正し、ファイルを上書き保存します。

[ORACLE_HOME]/network/admin/sqlnet.ora

...(元々の記述)

WALLET_LOCATION = (SOURCE = (METHOD = file) (METHOD_DATA = (DIRECTORY="C:\\Users\\hrgm\\OracleCloud\\Wallet")))

Windows の場合、 DIRECTORY に指定するクライアント資格証明 (ウォレット) のファイル名に拡張子は付けません。 また、バックスラッシュはエスケープする必要があるので注意してください。

tnsnames.ora (TNS 構成ファイル) の更新

sqlnet.ora と同様に、解凍したクライアント資格証明 (ウォレット) の中にある tnsnames.oraテキストエディタで開き、クライアント PC の tnsnames.ora へ追記します。

[クライアント資格証明 (ウォレット)]/tnsnames.ora

(全文)

こちらは sqlnet.ora と違い編集は不要なので、クライアント PC の tnsnames.ora の最終行へそのまま全文をコピー & ペーストして大丈夫です。

[ORACLE_HOME]/network/admin/tnsnames.ora

...(元々の記述)

(最終行へ全文を貼り付け)

※ セキュリティのため、 tnsnames.ora の内容は記載していません。

Oracle Database ツールでの接続

SQL*Plus

tnsnames.ora に登録されていれば、あとはいつも通りネットワーク・サービス名 (net_service_name) を使用してデータベースへ接続できます。

1. SQL*Plus 起動

PowerShell

sqlplus /nolog

2. ローカル・ネーミング・メソッドによるデータベース接続

CONNECT ADMIN/[password]@[net_service_name]

ユーザ名に ADMIN 、パスワードには Autonomous Transaction Processing のプロビジョニングの際に決めたパスワードを入力します。

ネットワーク・サービス名は、Autonomous Database には予め 5 種類のデータベース・サービス名が用意されており、その中から選択する形になります。

データベース・サービス名
[database_name]_tpurgent
[database_name]_tp
[database_name]_high
[database_name]_medium
[database_name]_low

使用するデータベース・サービス名によってパフォーマンスや同時実行性が異なりますので、詳細については公式ドキュメントをご覧ください。

docs.oracle.com

データベース接続後は、通常の Oracle Database と同じように SQL を発行することができます。

SQL Developer

SQL Developer でクライアント資格証明 (ウォレット) を使用してデータベースを接続する場合は、専用の接続タイプを指定して接続を行います。

1. データベース接続の作成/選択ダイアログを開く

SQL Developer を起動し、接続の作成ボタン (以下の画像の赤枠内) を押下して、データベース接続の作成/選択ダイアログを開きます。

データベース接続の作成

2. ユーザ情報を入力

ユーザ名に ADMIN 、パスワードには Autonomous Transaction Processing のプロビジョニングの際に決めたパスワードを入力します。

接続タイプは クラウド・ウォレット を選択し、構成ファイルにクライアント資格証明 (ウォレット) ファイルを指定します。

サービスには前述した SQL*Plus と同様、事前定義されている 5 種類のデータベース・サービス名から選択します。

ユーザ情報を入力

3. 接続

あとは接続ボタンを押下すれば、 SQL Developer から Autonoumous Transaction Processing のデータベースへ接続できます。

接続後は SQL*Plus と同様に、通常の Oracle Database のように各種操作を行うことができます。

クライアントアプリケーションからの接続

その他のクライアントアプリケーションからの接続に関しては、 SQL*Plus と同様にネットワーク・サービス名を使用したローカル・ネーミング・メソッドによるデータベース接続を行うことで、特に Cloud を意識することなく使用することができるはずです。

クライアント資格証明 (ウォレット) を使用しているネットワーク・サービス名さえ指定すれば、あとは今まで通り使用できると思いますが、詳細については使用するクライアントアプリケーションやライブラリの公式ドキュメントなどを確認してください。

おわりに

クラウドというだけで何となく難しいイメージを持ってしまっていましたが、実際は Web ブラウザを介して GUI で簡単に操作・管理ができ、クライアントからの接続についても特に難しいことはありませんでした。

今まではローカルの PC に Oracle Database をインストールして使用していたため、 PC を替える度にデータベースの作成・データ移行をする必要がありましたが、クラウドへ移行したことによってそういった手間から解放されると共に、物理破損によるデータ消失のリスクも軽減することができました。

それもこれも、永久無料 (常時無償) の Oracle Cloud Free Tier という Oracle の超太っ腹なサービスがあったからこそ実現できたもので、他にクラウドRDBMS で永久無料なのは Heroku Postgres くらいしかないため、 Oracle 様には足を向けて寝られません。

Oracle Cloud は AWSGCP の影に隠れがちですが、Oracle 大好きな僕としては是非ともシェアを伸ばしていってほしいと思います。

そして、願わくばシェアが伸びた暁には、もっと Free Tier の無料枠を拡大してくれることを… (*´∀ˋ人)

【Flask】ディレクトリ構成を考える

flask-logo

追記

- 2020/09/13 -

コメント欄で msiz07 様からご指摘頂きました通り、当記事で Flask 公式ドキュメント としている参照先はすべて、正確には Flask 公式ドキュメント (本家英語版) を個人の方が和訳したもの になります。

厳密に言えば参照先は 非公式 となりますが、内容に関しては本家の英語版と同様に大変参考になるものですので、非公式であるという注釈を追記した上で引続き参照させていただいております。

msiz07 様、コメントありがとうございました!


Flask 公式ドキュメント (本家英語版)

flask.palletsprojects.com

はじめに

Flask は Python 用 Web アプリケーションフレームワークの一つで、自身をマイクロフレームワーク (microframework) と呼んでいます。

Flask 公式ドキュメント ( *1 ) にもある通り、マイクロフレームワークの意味するところは、必要最小限の機能を提供しつつも多くの拡張性を保持することを目的としており、シンプルが故に機能が不足するということではありません。

シンプルなのは良いことですが、同じ Python 用 Web アプリケーションフレームワークである Django のように決まったディレクトリ構造を持たないため、目的に応じて自由にディレクトリやファイルを構成できる利点はあるものの、一定規模以上の Web アプリを作成しようとしたり複数名での開発を行う場合はある程度決まったルールが必要です。

ということで、今回はユースケースに応じた Flask アプリケーションのディレクトリ構成を考えてみました。

TL;DR

  • 必要なことはすべて Flask 公式ドキュメントが教えてくれた
  • Prototype 用、 Web アプリ用、 RESTful API 用の 3 パターンを考えたよ
  • GitHub にアップしたよ

github.com

動作環境

OS Version
Windows 10 Pro 1909
Application Version
PowerShell 5.1.18362.752
Language Version
Python 3.8.5
Package Version
Flask 1.1.2

前提条件

  • venv で仮想環境が構築済みで、 Activate された状態である
  • 仮想環境に Flask がインストール済みである

ユースケース毎のディレクトリ構成を考える

Flask アプリケーションはモジュールとして作るか、パッケージとして作るかでディレクトリ構成が大きく変わります。

プロトタイプ開発や小規模な Web アプリであれば、必要最小限の構成でもさほど煩雑にはならないため、単一モジュールに処理を詰め込んでもいいですが、ある程度スケールすることが見込まれる場合は複数の処理を意味のある単位に分割し、パッケージとして Flask アプリケーションを作るのが定石です。

msiz07-flask-docs-ja.readthedocs.io

個人的には、機能が複数 (例えば、検索・一覧表示機能と登録・更新機能など) になる場合は、初めからパッケージとしてモジュールを分割しておいた方が、後々のことを考えると無難だと思っています。

今回はユースケースに分けて、プロトタイプ開発や小規模 Web アプリケーションをターゲットするモジュール版、一定規模を想定したパッケージ版に加え、 SPA (Single Page Application) などからのアクセスに対応する RESTful API 版の 3 パターンを考えてみました。

プロトタイプ・小規模 Web アプリケーション

エントリーポイント (FLASK_APP 環境変数) をモジュールとして、ルーティングやビジネスロジックは一つの Python ファイルにまとめて記述します。

使い捨てとなるプロトタイプや単機能の Web アプリケーションなどを想定した、さっと手軽に作れるシンプルなディレクトリ構成です。

ディレクトリ構成

/
├ instance/
│ └ config/
│    ├ development.py
│    └ production.py
├ log/
├ static/
│ └ css/
│    └ default.css
├ templates/
│ ├ base.html
│ └ greeting.html
├ tests/
├ application.py
├ requirements.txt
└ settings.py

/instance/

Flask 0.8 から導入された Instance Folders (インスタンスフォルダ) 。

バージョン管理 (Git など) に含めない設定ファイルや非公開ファイル (API キーなど) を格納するディレクトリ。

Flask のインスタンスを生成する際、 instance_path パラメータや instance_relative_config パラメータでディレクトリの位置を設定できる。

msiz07-flask-docs-ja.readthedocs.io

/log/

Flask のログ出力先ディレクトリ。

ログ出力については、この記事では触れません。

/static/

JavaScriptCSS 、画像ファイルなどの静的ファイルを格納するディレクトリ。

msiz07-flask-docs-ja.readthedocs.io

/templates/

テンプレート (HTML) ファイルを格納するディレクトリ。

Flask はテンプレートエンジンとして Jinja を採用している。

msiz07-flask-docs-ja.readthedocs.io

/templates/base.html

<!DOCTYPE html>

<head lang="ja">
  <meta charset="utf-8" />
  <link rel="stylesheet" href="{{ url_for("static", filename="css/default.css") }}">
  <title>{% block title %}{% endblock %}</title>
</head>

<body>
  {% block content %}{% endblock %}
</body>

/templates/greeting.html

{% extends 'base.html' %}

{% block title %}flask-web-app{% endblock %}

{% block content %}
<h1>
  {{ greeting }} {% if user_name %}{{ user_name }}{% else %}World{% endif %} !!
</h1>
{% endblock %}

/tests/

UNIT テスト用ファイル格納ディレクトリ。

テストについては、この記事では触れません。

/application.py

エントリーポイントとして Flask インスタンスを生成する Python モジュール。

import os
from flask import Flask, request, render_template

app = Flask(__name__, instance_relative_config=True)

# 標準設定ファイル読み込み
app.config.from_object("settings")

# 非公開設定ファイル読み込み
if app.config["ENV"] == "development":
    app.config.from_pyfile(os.path.join("config", "development.py"), silent=True)
else:
    app.config.from_pyfile(os.path.join("config", "production.py"), silent=True)

@app.route("/", methods=("GET", "POST"))
@app.route("/<string:greeting>", methods=("GET", "POST"))
def greeting_user(greeting="Hello"):
    if request.method == "POST":
        user_name = request.form["user_name"]
    else:
        user_name = request.args.get("user_name", "")

    return render_template("greeting.html", greeting=greeting, user_name=user_name)

設定ファイル

標準設定

/settings.py

application.pyapp.config.from_object() メソッドから読み込まれる標準設定情報を記述。

バージョン管理に含まれるため、公開しても問題無い設定や後から上書きされるデフォルト設定のみを記述し、データベース接続情報や各種 API キーなどの非公開情報は記述しない。

非公開設定

/instance/config/development.py

/instance/config/production.py

application.pyapp.config.from_pyfile() メソッドから読み込まれる非公開設定情報を記述。

データベース接続情報や各種 API キー、 Flask の組み込みの設定値 SECRET_KEY もここへ記述する。

development.py は開発用の設定、 production.py には本番用の設定を記述し、 app.config["ENV"] の値によって設定を切り替える。

起動方法

PowerShell でアプリルートをカレントディレクトリとして flask run を実行すると、開発用サーバが起動します。

開発用サーバ起動前に、環境変数 FLASK_APP に Flask アプリケーション (インスタンス) が生成される Python モジュールを指定し、デバッグモードを有効にするために環境変数 FLASK_ENV には development を指定します。

PowerShell

> $env:FLASK_APP="application.py"
> $env FLASK_ENV="development"
> flask run

動作確認

Web ブラウザで http://127.0.0.1:5000/Hi?user_name=hrgm を開くと、起動した Flask アプリケーションへアクセスできます。

ちなみに、今回のサンプルは URL のパラメータを表示しているので、 Hihrgm を変更すると表示内容が変わります。

中規模 Web アプリケーション

エントリーポイントをパッケージとし、 Blueprint を用いてルーティングやビジネスロジックを別モジュールへ分割しています。

msiz07-flask-docs-ja.readthedocs.io

機能単位にモジュールを分割することで、今後の拡張によってコードが煩雑になることを防ぎます。

ディレクトリ構成

/
├ app/
│ ├ models/
│ ├ static/
│ │ └ css/
│ │    ├ default.css
│ │    └ style.css
│ ├ templates/
│ │ ├ base.html
│ │ └ greeting/
│ │    └ greeting.html
│ ├ views/
│ │ └ greeting.py
│ └ __init__.py
├ instance/
│ └ config/
│    ├ development.py
│    └ production.py
├ log/
├ tests/
├ MANIFEST.in
├ requirements.txt
├ settings.py
└ setup.py

※ 前述の プロトタイプ・小規模 Web アプリケーションのディレクトリ構成 との差分のみ記述します。

/app/

Flask アプリケーションのコードを含むディレクトリ。

機能別にモジュールを分割するため、 Python パッケージとする。

/app/models/

データベース接続や Model クラスを記述したファイルを格納するディレクトリ。

MTV (Model Template View) モデルの Model に相当するファイルを格納する。

データベース関連は、この記事では触れません。

/app/views/

ルーティングや、その URL に対するコールバック (ビジネスロジック) を記述したファイルを格納するディレクトリ。

MTV (Model Template View) モデルの View に相当するファイルを格納する。

/app/views/greeting.py

from flask import Blueprint, render_template, request

greeting = Blueprint("greeting", __name__, template_folder="templates")

@greeting.route("/", methods=("GET", "POST"))
@greeting.route("/<string:greeting>", methods=("GET", "POST"))
def greeting_user(greeting="Hello"):
    if request.method == "POST":
        user_name = request.form["user_name"]
    else:
        user_name = request.args.get("user_name", "")

    return render_template(
        "greeting/greeting.html", greeting=greeting, user_name=user_name
    )

/app/__init__.py

本来、 __init__.py には Python パッケージの初期化処理を記述しますが、特に Flask アプリケーションのエントリーポイントとなるパッケージでは、 Application Factory となる create_app() メソッドを定義します。

また、 Blueprint によって分割された views ディレクトリ配下の greeting モジュールから Blueprint オブジェクト (greeting) を import し、 Flask アプリケーションへ登録しています。

import os
from flask import Flask

def create_app(test_config=None):
    app = Flask(__name__, instance_relative_config=True)

    # 標準設定ファイル読み込み
    app.config.from_object("settings")

    # 非公開設定ファイル読み込み
    if app.config["ENV"] == "development":
        app.config.from_pyfile(os.path.join("config", "development.py"), silent=True)
    else:
        app.config.from_pyfile(os.path.join("config", "production.py"), silent=True)

    if test_config is not None:
        # テスト用設定を上書き
        app.config.from_mapping(test_config)

    from .views.greeting import greeting

    app.register_blueprint(greeting, url_prefix="/greeting")

    return app

/MANIFEST.in

当プロジェクトを配布・インストールする際に必要となるマニュフェストテンプレート。

/setup.py

当プロジェクトを配布・インストールする際に必要となる setup スクリプト

起動方法・動作確認

アプリルートで flask run を実行して開発用サーバが起動するのは前述の プロトタイプ・小規模 Web アプリケーションの起動方法 と同じです。

ただし、開発用サーバ起動前に設定する環境変数 FLASK_APP には、 Application Factory が定義されている Python パッケージを指定します。

PowerShell

> $env:FLASK_APP="app"
> $env FLASK_ENV="development"
> flask run

動作確認は前述の プロトタイプ・小規模 Web アプリケーションの動作確認 と同様です。

RESTful API

基本的には 中規模 Web アプリケーション と同様のディレクトリ構成ですが、レスポンスはすべて JSON 形式となるため、 static ディレクトリや templates ディレクトリは省いています。

ディレクトリ構成

/
├ api/
│ ├ models/
│ ├ views/
│ │ └ greeting.py
│ └ __init__.py
├ instance/
│ └ config/
│    ├ development.py
│    └ production.py
├ log/
├ tests/
├ MANIFEST.in
├ requirements.txt
├ settings.py
└ setup.py

※ 前述の プロトタイプ・小規模 Web アプリケーションのディレクトリ構成中規模 Web アプリケーションのディレクトリ構成 との差分のみ記述します。また、 Flask アプリケーションのディレクトリ名は api に変更しています。

/api/views/greeting.py

URL にバインドされたコールバック関数の戻り値を dict (辞書型) にすると、レスポンスの mimetypeapplication/json となり、 JSON データとして送信されます。

from flask import Blueprint, request

greeting = Blueprint("greeting", __name__)

@greeting.route("/", methods=("GET", "POST"))
@greeting.route("/<string:greeting>", methods=("GET", "POST"))
def greeting_user(greeting="Hello"):
    if request.method == "POST":
        user_name = request.form["user_name"]
    else:
        user_name = request.args.get("user_name", "")

    return {"greeting": greeting, "user_name": user_name}

起動方法・動作確認

前述の 中規模 Web アプリケーションの起動方法・動作確認 と同様。

GitHubリポジトリ

今回まとめた 3 パターンのディレクトリ構成は、 GitHubリポジトリを公開しています。

今後 Flask で開発をする際はプロジェクトテンプレートとして使用し、使いながらその都度、修正・改善をしていこうと思います。

github.com

おわりに

Flask アプリケーションのディレクトリ構成を考えるにあたり、様々なサイトを参考にさせていただきましたが、必要なことは Flask 公式ドキュメント にすべて書いてありました (当たり前) 。

僕は DjangoRails といった Web アプリケーションフレームワークをほんの少しかじった程度ですが、そういった大規模開発に対応できるフレームワークを模倣することで、 Flask の標準機能だけでもある程度の開発規模に耐えうるディレクトリ構成が実現可能だと思いました。

プロトタイプや小規模 Web アプリケーションの開発には手頃でちょうど良いと書きましたが、 Django よりも Flask の方が制約や規約が少なく自由度が高いため、機能数や画面数は少ないけれど複雑で特殊な処理が多いってケースには、多少規模が大きくなっても逆にフィットするかもしれません。

保守は大変になりそうですけど。。

いずれにしても適材適所なので、現在の Python の Web アプリケーションフレームワークである Django と Flask の二強体制は、開発規模によって使い分けるにはちょうど良い形なのかなと思っています。

が…!

Flask について調べた直後ではありますが、最近は巷で話題の FastAPI に興味津々です!!

fastapi.tiangolo.com

近いうちに使ってみたい ( º﹃º` )

*1:参照先は本家の Flask 公式ドキュメント (英語版) を個人の方が翻訳した日本語版。以降、 Flask 公式ドキュメントと記述されているものはすべて同様。

【働き方】2020 年上半期のリモートワークを振り返る

リモートワーク

はじめに

キラッキラしたイケてる Web 系エンジニアみたいに、軽快なバックグラウンドミューズィックが流れるシャレオツなカフェや自宅のデスクで MacBook を開き、クールなフェイスでホットなラテを飲みながら颯爽とコードを書きたい。

そう思っていた時期が僕にもありました。

近年は現実を知ったこともあり、今は自分にできることを積み重ねていこうと心に決めたわけですが、それでもずっと思っていたことがあります。

リモートワークがしたい!!

時は金なり (Time is money) と昔の偉い人が言った通り、自宅と会社の往復に毎日 2 時間以上かけるのは、どう考えても時間を無駄にしています。

そうは言っても SES を生業とする SIer の弊社は、多くのケースにおいて常駐先の勤務体制に従うほかなく、リモートワークは夢のまた夢という感じでした。

しかし 2020 年に入り、受託開発やコロナ渦による影響で思いもよらずリモートワークをする機会を得て、フルではないものの一部リモートワークを 7 月現在も約半年間続けることができているので、ここで一度振り返りをしておこうと思い、このエントリーを書いています。

リモートワークの内容

受託開発

期間 時間 場所 備考
2020 年 1 月 ~ 4 月 定時後 (残業扱い) 自宅 フルリモート (必要に応じて対面でのミーティング)

2020 年 1 月下旬、普段は客先常駐のプロジェクトに参画している僕ですが、それと並行して受託開発のプロジェクトにリモートワークで参画することになりました。

開発環境は Google Cloud 、メンバーとのやり取りは Slack を使用し、会社から帰宅後に自宅でリモートワークという形です。

作業する日と時間は各自の裁量に一任されましたが、 36 協定を遵守した残業扱いなので決して違法ではありませんw

約 3 ヶ月間のプロジェクトで初めてのリモートワークでしたが、日中は普通にフルタイムで会社に出社しているので、感覚的には自宅でプライベートなコードを書いている時とあまり変わらなかったです。

個人開発と違う点は、チーム内でコミュニケーションを取りながら開発を行う必要があることと明確にスケジュールが決まっていること (納品する相手がいるという意味) で、チーム内でのコミュニケーションについてはいくつか課題もありました。

課題があるとはいっても、肝心のプロジェクトの方は初めてのリモートワークにも関わらず無事にリリースまで完了し、リモートワークでも十分結果が出せるということを証明できました。

【課題】チーム内コミュニケーション

裁量労働によるコミュニケーションロス

コミュニケーションツールとして Slack を使用しましたが、 普段の常駐先プロジェクトでも MicrosoftTeams を使用しているので、ツールの違いによる影響はほとんどありませんでした。

問題はツールではなく運用する人の側にあり、作業する日と時間が各自の裁量に任せられたことで、優先度の高い確認事項があったとしても返答がいつ来るか分からないということが度々発生しました。

これが定時内 (所定の就業時間内) を前提としたリモートワークであれば、当日中か遅くとも翌営業日には何らかの返答を期待できますが、定時後の残業扱いとなっている以上、連絡した相手がいつ確認するか分かりません。

そもそも残業時間のみを前提とした働き方がイレギュラーだというのは重々承知していますが、顔の見えない非同期コミュニケーションにおいて、いつ返答がくるか分からないのはなかなかストレスが溜まるものでした。

SlackTeams はメンバーのログイン状況が分かるようになっているので、双方がオンラインであればチャットはとても有効なコミュニケーションツールになりますが、一方がオフラインの場合はチャットもメールも、なんなら留守録のメッセージであっても、いつ気付いて返信をくれるか分からないという点においては大差無いということになります。

これはリモートワークに限った話ではありませんが、裁量労働のフルリモートワークでは、他者へ確認・調整が必要なことは先を見越して事前に連絡を済ませるなど、自分の作業が円滑に進むようにセルフマネジメントを行うことが特に重要だと感じました。

今回はコミュニケーションツールとして Slack しか使用しませんでしたが、次回からは緊急度に応じた複数の連絡手段を用意したり、チーム内で各メンバーの数日~ 1 週間先くらいまでのスケジュールを共有するなどの、ガチガチにならない程度の運用ルールを決めるべきかと思います。

SES

期間 時間 場所 備考
2020 年 5 月 ~ 継続中 定時内 自宅 週 2 ~ 3 日 (残りの平日は出社)

2020 年 4 月、新型コロナウイルス (COVID-19) の影響により発令された緊急事態宣言を受け、僕が参画するプロジェクトも客先の意向で 5 月中旬から本格的にリモートワークの導入が始まり、 7 月現在も週 2 日は自宅でリモートワークを行っています。

SES でリモートワークってあまり聞いたことが無いと思いますが、貸与されたノート PC から VPN で社内ネットワークへ接続して業務を行うため、開発環境やその他は出社した場合と比べて何ら変わりありません。

勤務時間も出社時と同様なので、リモートワークであっても定時内はメンバー全員がオンラインになっています。

コミュニケーションツールは MicrosoftTeams を使用していますが、前述の受託開発プロジェクトとは違い、コミュニケーションロスはほとんど発生していません。

現時点での唯一の課題は、社内ネットワークへの接続とセキュリティを確保するための VPN が非常に低速な (そもそも社内ネットワーク自体も遅い) ことで、実用上に大きな問題は無いものの、 Teams のビデオ会議はネットワーク帯域を圧迫するという理由から使用が禁止されています。

未だ新型コロナウイルスの問題は収束しておらずリモートワークの終了時期は未定ですが、あくまでも緊急的な措置としてのリモートワークなので、おそらく今以上の環境の改善は期待できないと思います。

しかし、前述の受託開発と同様、リモートワークであっても環境さえ整えば、出社時と比べて遜色ないパフォーマンスを発揮できるということが分かりました。

【課題】ネットワーク (通信環境)

ネットワーク帯域幅の不足

リモートワークでは作業の多くがネットワークを介して行われることから、十分なネットワーク帯域幅が確保されていないと、それがボトルネックとなって業務に支障をきたす可能性が出てきます。

自宅でリモートワークをする場合、一般的には 1 Gbps 以上の光回線のインターネット接続を契約しているケースが多いと思いますので、その点は問題無いと思います。

しかし、多くの企業はセキュリティ等を考慮して VPN専用線を使用することが前提となっており、その帯域幅が問題になってきます。

この問題の難しい点は、ネットワーク帯域幅の確保はコスト増に直結するため、個人やチームではなく会社を動かさなければならない点にあります。

これはシンプルな割に解決が難しく、いかに会社の上層部を説得できるかがポイントになります。

リモートワークをしてみて

単純な感想

前々からリモートワークがしたいと考えていた僕が、実際に約半年間のリモートワークをしてみて言えることは、

リモートワーク最高~~~~~~~~~

リモートワーク最高

の一言に尽きます。

朝ご飯はちゃんと食べられるし、子供を学校に送り出すこともできるし、仕事中に淹れたての珈琲は飲めるし、疲れたらゴロンと寝っ転がれるし、何より通勤時間がゼロになるので今まで通勤に充てていた時間を有効活用することができます。

業務上の問題も現時点では出ていないですし、個人レベルの話ではありますが、部分的なリモートワークであれば導入は難しくないなと思いました。

リモートワークのメリット

通勤時間がゼロ

通勤時間がゼロということは、今まで通勤にかかっていた時間を別のことに充てられることを意味します。

単純に自分の時間が増えるので、勉強をするもよし、家族との時間を増やすのもよし、体調が悪いときは休息を取ることもでき、とにかく QOL が爆上がりします。

また、エンジニアであれば仕事モードの集中力を保ったまま、業務での開発から個人開発へシームレスに移行できることにメリットを感じる方もいるのではないでしょうか。

次で述べている私用 PC の併用もあり、アウトプットの時間と質の確保にも大きく寄与します。

私用 PC を併用できる

今まで、有用な情報が記載されているサイトのブックマークや公開情報をまとめた自分用資料は会社の PC にしか存在せず、たとえ個人情報などの機密情報が含まれていなくても、持ち出しや印刷しての持ち帰りは不可能でした。

また、 SES では多くの場合、外部ネットワークへのアクセスが監視・制限されており、下手なサイトにアクセスすると呼び出されて事情聴取を受けることも間々あります。

業務要件や規則によって違ってくるとは思いますが、リモートワークであれば業務用に PC を貸与されていても、調べものは私用 PC を使用することができるので、調べた公開情報を自分の PC に残して、あとで改めて勉強のために見返したりまとめたりすることができます。

これは客先常駐による弊害の一つで、自身のスキルアップを妨げる一因にもなっていましたが、リモートワークではキレイさっぱり取り除かれました。 (スキルアップできるとは言ってない)

リモートワークの課題

ここはあえてデメリットではなく解決が可能な課題と書きますが、今後想定されるものとしては 新人教育 が課題の一つになってくるかと思います。

今回のリモートワークはすべて見知ったメンバー同士のプロジェクトであったため、たとえ顔が見えなくてもテキストから大体の表情や口ぶりまで想像することができましたが、新規参画メンバーやそれこそ新入社員を相手にする場合は相当な配慮が必要になるでしょう。

顔の見えない相手と非同期的にテキストベースでコミュニケーションを取るには、誤解を与えない言い回しや簡潔に用件をまとめる文章力などに加え、相手の心情を汲み取る気遣いも要求されます。

特に教育の場にあっては、対面であれば態度や雰囲気から相手の理解度などを何となく察することができますが、リモートワークではそうもいきません。

教えた内容の理解度を都度確認する、教育を受けている側も態度や表情では何も伝えられないので、はっきりと「分かった」「分からない」の意思表示をするなど、人によっては遠慮してしまうケースが出てくるかもしれません。

そもそもすべてをリモートで行う必要はないので、集合教育やビデオ会議も適宜取り入れ、チームに合った最適解を見付けることが大切なんだと思います。

おわりに

この半年間の体験は、何故今まで律儀に出社していたのかと後悔さえしてしまうほど (規則だからです) で、リモートワークの魅力を感じるには十分過ぎるものでした。

同時にいくつかの課題も見えてきましたが、これらは解決不可能な問題ではなく、個人やチームの努力と会社の協力さえ得られればいくらでも解決は可能です。

今回振り返った内容を踏まえ、 2020 年下半期は弊社の正式な制度としてリモートワークを導入できるように、働きかけを行っていく所存です!

弊社では、というか僕が、一緒に開発エキスパートチームを作ってくれる仲間を募集しています!

現在構想中の開発スペシャリストチーム (名前ブレブレ) は、各領域のエキスパートをメンバーとする少数精鋭チーム (予) で、リモートワーク (予) 中心に開発を行うことを予定しています (すべて予定)。

気になる方は是非ご連絡をお待ちしています ٩(ˊᗜˋ*)و

【Angular】データバインディングを整理する

angular-logo

はじめに

Angular のデータバインディングにはいくつかの種類と書き方があり、未だに「この場合はどう書いたらいいんだっけ?」となることがあるので、今回はデータバインディングについて整理してみました。

当記事では Angular のデータバインディングを一通りまとめていますが、詳細な仕組みやすべての記法を網羅はしていません。

どういう時にどのデータバインディングを使うかを確認したら、必要に応じて Angular 公式ドキュメントの該当するバインディングの詳細を読みましょう。

angular.jp

もし Angular 公式ドキュメントを読んで難しいと感じる場合は、先に以下の書籍を一読することをお勧めします。

Angularアプリケーションプログラミング

Angularアプリケーションプログラミング

僕が初めて Angular を学んだのもこの本で、今となっては一部古い内容もありますが (2020/06/18 現在の Angular の最新バージョンは 9 、書籍の Angular のバージョンは 4) 、大半は今でもそのまま使えるものなので、一通り読んでからの方がすんなり Angular 公式ドキュメントの内容を理解することができると思います。

対象読者

  • Angular のデータバインディングを一通り学んだが、すべてを使いこなすレベルには至っていない方
  • Angular のデータバインディングは、とりあえずプロパティをブラケットで囲めば何とかなると思っている方

前提条件

  • Angular CLI がインストールされていて、アプリケーションを既に作成済み

動作環境

OS Version
Windows 10 Pro 1909
Environment Version
Node.js 12.18.0
npm 6.14.4
Package Version
@angular/cli 9.1.6

データバインディング

Angular のデータバインディングは、テンプレート (ビュー) とコンポーネント (クラス) 間で相互にデータをやり取りするための仕組みです。

片方向 (片方向バインディング) と双方向 (双方向バインディング) のデータフローが存在し、片方向バインディングにはデータタイプやターゲットによって複数の方法が用意されています。

Binding type Data flow
Interpolation (補間) コンポーネント → テンプレート
プロパティ コンポーネント → テンプレート
属性 コンポーネント → テンプレート
クラス コンポーネント → テンプレート
スタイル コンポーネント → テンプレート
イベント テンプレート → コンポーネント
双方向 コンポーネント ⇔ テンプレート

通常、データバインディングによってバインドできる値のコンテキストはコンポーネントインスタンスで、 windowdocument といったグローバル名前空間内のものは参照できません。

以降は、以下のコードをベースとして、各データバインディングについてまとめていきます。

app.module.ts

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';

import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';

@NgModule({
  declarations: [AppComponent],
  imports: [
    BrowserModule, 
    AppRoutingModule
  ],
  providers: [],
  bootstrap: [AppComponent],
})
export class AppModule {}

app.component.ts

import { Component } from '@angular/core';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})
export class AppComponent {}

app.component.html

<h1 class="red">
  Hello Angular !!
</h1>

app.component.scss

.red {
  color: #ff0000;
}

Interpolation (補間) 構文

Data flow
Component → Template

Component

...
export class AppComponent {
  title = 'Angular';
}

Template

<h1 class="red">
  Hello {{ title }} !!
</h1>

コンポーネントクラスの Public プロパティを二重中括弧で囲んでテンプレートに記述すると、プロパティの値を文字列値としてテンプレート内に展開できます。

TypeScript のクラスのメンバのアクセス修飾子は、 public がデフォルトとなっているので省略可能です。

また、コンポーネントクラスのメソッドや一部制限はあるものの一般的な JavaScript 式も記述することができ、その場合は二重中括弧内の評価結果が文字列に変換され、テンプレート内に展開されます。

プロパティに対する Interpolation (補間)

Component

...
export class AppComponent {
  frameworkName = 'Angular';
}

Template

<input type="text" name="framework" value="{{ frameworkName }}">

多くの場合、後述するプロパティバインディングが主に使用されますが、 DOM プロパティに対して Interpolation (補間) 構文で値をバインドすることも可能です。

プロパティバインディング

Data flow
Component → Template

Component

...
export class AppComponent {
  imgSrc = '../assets/images/angular.png'
  isDisabled = true;
}

Template

<img [src]="imgSrc">
<button type="button" [disabled]="idDisabled">OK</button>

ターゲット要素の DOM プロパティをブラケット (角括弧) で囲み、バインドする値を指定します。

バインドする値は Interpolation (補間) 構文と同様に、コンポーネントクラスの Public メンバや式が記述できます。

注意として、バインディングの対象は DOM プロパティであって、 HTML 属性ではありません

たとえ同じ名前であっても、 DOM プロパティと HTML 属性は別物であることを理解しておきましょう。

ja.javascript.info

プロパティバインディングは DOM 要素のプロパティだけでなく、コンポーネントやディレクティブのプロパティに対して行うことも多いです。

属性バインディング

Data flow
Component → Template

Component

...
export class AppComponent {
  colSpan = 2;
}

Template

<table>
  <tr>
    <th [attr.colspan]="colSpan">Angular</th>
  </tr>
  <tr>
    <td>component</td>
    <td>template</td>
  </tr>
</table>

前述の通り、バインディングの対象は基本的にプロパティですが、 DOM プロパティが存在せず、 HTML 属性しか存在しない場合は属性バインディングを使用します。

ターゲット要素の HTML 属性をブラケット (角括弧) で囲み、属性名にドット (.) 付きの接頭辞 attr を付与して、バインドする属性値を指定します。

バインドした値が null の場合、その属性は削除されます。

クラスバインディング

Data flow
Component → Template

Component

...
export class AppComponent {
  bold = true;

  divClass = {
    bold: true,
    blue: false
  };
}

Template

<div class="red" [class.bold]="bold">
  Red and bold
</div>

<div class="red" [class]="divClass">
  Bold only
</div>

Style

.red {
  color: #ff0000;
}

.bold {
  font-weight: bold;
}

.blue {
  color: #0000ff;
}

ターゲット要素のクラス名をブラケット (角括弧) で囲み、ドット (.) 付きの接頭辞 class を付与して、バインドする値を指定します。

バインドされた値は論理型 (boolean 型)として評価され、真 (true) の場合はクラスを追加、偽 (false) の場合はクラスが削除されます。

前述のプロパティバインディングでも class 属性に値をバインドできますが、元々指定されている静的な値が存在した場合はバインドした値で上書きされてしまいます。

クラスバインディングを使用すれば、 class 属性に必ず設定したい静的な値はそのままに、クラスの追加・削除を行うことが可能です。

スタイルバインディング

Data flow
Component → Template

Component

...
export class AppComponent {
  divHeight = '150px';
  divWidth = 300;
  divBackgroundColor = '#ff0000';

  divStyle = {
    width: '200px',
    height: '200px',
    backgroundColor: '#0000ff'
  };
}

Template

<div 
  [style.height]="divHeight" 
  [style.width.px]="divWidth" 
  [style.background-color]="divBackgroundColor"
>
  Red rectangle
</div>

<div [style]="divStyle">
  Blue square
</div>

ターゲット要素のスタイルプロパティをブラケット (角括弧) で囲み、ドット (.) 付きの接頭辞 style を付与して、バインドする値を指定します。

通常はバインドする値は文字列ですが、単位を必要とするスタイルの場合、スタイル名の後ろにドット (.) 付きで単位を追加して数値をバインドすることも可能です。

また、スタイルプロパティは style.background-color のようなケバブケース (kebab-case) 以外に、 style.backgroundColor のようなキャメルケース (camelCase) でも記述できます。

クラスバインディングと同様に style プロパティへ複数のスタイルを一括でバインドすることができますが、適用されるスタイルの優先順位はより詳細なバインディングの方が上位となります。

<div [style]="divStyle" [style.height]="divHeight">
  This div's height is 150px.
</div>

イベントバインディング

Data flow
Template → Component

Template

<button type="button" (click)="showAlert($event)">表示</button>

Component

...
export class AppComponent {
  showAlert(e: MouseEvent): void {
    e.preventDefault();
    e.stopPropagation();

    alert((e.target as HTMLButtonElement).textContent + 'が押されました');
  }
}

ターゲット要素のイベントを丸括弧で囲み、バインドする文を指定します。

イベントが発生するとバインドされた文が実行され、 $event にはイベントに応じたオブジェクトが格納されます。

ターゲットが DOM 要素イベントの場合、 $event は DOM イベントオブジェクトになります。

developer.mozilla.org

また、一般的なイベントリスナーと同様に preventDefault()stopPropagation() を使用して、デフォルト動作やバブリングをキャンセルすることが可能です。

双方向バインディング

Data flow
Template ⇔ Component

双方向バインディングでは、バインドされた値に対し、コンポーネントクラスとテンプレートそれぞれで行った変更を相互に反映させることができます。

前述のプロパティバインディングとイベントバインディングの組み合わせと専用の構文を使用して、相互にデータの変更を共有します。

代表する書き方として、ターゲット要素のプロパティをブラケット (角括弧) と丸括弧で囲む構文 [(property)] がありますが、こう書きさえすれば双方向バインディングが実現できると誤解されがちなので注意してください。

よくある誤った書き方

Component

...
export class AppComponent {
  yourName = 'hrgm';
}

Template

<input type="text" name="your_name" [(value)]="yourName">
<span>Hello {{ yourName }} !!</span>

この場合、テキストボックスの valueyourName の値はバインドされますが、テキストボックスの値を変更しても yourName の値は変更されません。

双方向バインディングは、双方向に値を反映させるためのルールに則って書かれたコンポーネントのプロパティを、ブラケット (角括弧) と丸括弧 [(property)] で囲むことで初めて実現されます。

基本的な書き方

FeatureComponent

Template

<input type="text" [value]="name" (input)="inputName($event.target.value)">

Component

import { Component, EventEmitter, Input, Output } from '@angular/core';

@Component({
  selector: 'app-child-comp',
  templateUrl: './child.component.html',
  styleUrls: ['./child.component.css']
})
export class ChildComponent {
  @Input() name: string;
  @Output() nameChange = new EventEmitter<string>();

  inputName(_name: string) {
    this.name = _name;
    this.nameChange.emit(this.name);
  }
}

ターゲットとなるコンポーネントに、プロパティとそのプロパティ名に接尾語 Change を加えたイベントが存在する場合、双方向バインディングの構文が使用できます。

上記のコンポーネントには name プロパティと nameChange イベントが存在しているので、このコンポーネントname プロパティには双方向バインディング [(name)] が可能です。

Module

...
import { ChildComponent } from './child/child.component';

@NgModule({
  declarations: [
    AppComponent,
    ChildComponent
  ],
...

Template

<app-child-comp [(name)]="yourName"></app-child-comp>
{{ yourName }}

補足

つまるところ、双方向バインディングはプロパティバインディングとイベントバインディングシンタックスシュガー (糖衣構文) です。

<app-child-comp [name]="yourName" (nameChange)="yourName=$event"></app-child-comp>

Component

...
export class AppComponent {
  yourName = 'hrgm';
}

このように、双方向バインディングは使用できるプロパティが限定され、ネイティブな HTML 要素の属性やプロパティには使用することができません。

ネイティブな HTML のフォーム関連要素に対して双方向バインディングを行いたい場合は、 NgModel ディレクティブを使用することで双方向バインディングを実現することができます。

NgModel ディレクティブ (FormsModule) を使用する場合

Module

...
import { FormsModule } from '@angular/forms';

@NgModule({
  ...
  imports: [
    ...
    FormsModule
  ],
...

Component

...
export class AppComponent {
  yourName = 'hrgm';
}

Template

<input type="text" name="your_name" [(ngModel)]="yourName">
<span>Hello {{ yourName }} !!</span>

input 要素や select 要素などは NgModel ディレクティブを使用すると、簡単に双方向バインディングを実現することができます。

NgModel ディレクティブを使用するためには、 NgModule の imports リストに FormsModule を追加する必要があるので注意してください。

ngModel を含む Angular の組み込みディレクティブについては、また別記事でまとめる予定です。

おわりに

以上が Angular のデータバインディング全体の概要となります。

簡単にまとめたつもりでしたが、思った以上に長くなってしまいました…

当記事を書くにあたって Angular の公式ドキュメントを読み返しましたが、 Angular を使い始めた頃はあまり理解できていなかった内容や、既に忘れてしまっていることが多々あることに気付かされました。

普段当たり前のように書いているデータバインディングですが、実はもっと色々な書き方や使い方があり、それらを適切に使いこなすことができれば、さらにコーディングの幅を広げることができそうです。

JavaScript の三大フレームワークの一角とされる Angular ですが、素人目に見てもここ数年は React や Vue.js と比べて人気が下火になっている感は否めません。

しかし、フレームワークとしての機能が他よりも劣っていることは無いと信じているので、こういった振り返り学習をしながら、これからも Angular で開発を続けていきたいと思います。

と言いつつ、最近は Angular の前に使っていた Vue.js に戻ろうかと思ったりしてるんですけどねー!ww