1. フレームワーク作り
WebシステムをSPA(SinglePageApplication)を実現する上で大きな比重を占めるのは、フロントエンド側の実装部分です。労力の大半はここにつぎ込まれると言っても過言ではありません。しかしWebシステムを作る上では、バックエンド側との連携も必要となり、この部分が簡潔に書けるかどうかで、フロントエンド側の開発コストが決まります。ということはバックエンド側も重要なのです。
フロントエンドの描画部分に関しては、以前からJavaScript/TypeScriptでウインドウシステムを実現するフレームワークを開発しており、これを使えばかなり楽にSPAの開発が行えるようになっています。
ということで今回は、バックエンド側と通信部分をフレームワーク化する開発を行いました。
2. フレームワークの指針
- Node.js + TypeScript を使用
- バックエンド側の追加コードは超最小限になるように設計
- SPAなのでページ遷移は存在しないことが前提
- Cookieなにそれ美味しいの?(旧時代の遺物なので一切利用しない)
- セッションはブラウザ全体の情報と、タブごとの情報を分ける(タブごとに別ユーザの実現)
- フロントエンドとバックエンドの通信は、ローカルのファンクションを呼び出すがごとく簡単に記述できるようにする
3. Node.jsによるフレームワークの実装とサンプルアプリ
3.1 MyDNS-Explorer
フレームワークを作るには、何か適当なWebシステムを開発していくのが一番です。その中で必要となった機能を載せていけば良いのです。今回作ったのはこれです。
MyDNS-Explorer(ソースコード)
フロントエンドで使用しているJSWフレームワーク
Node.jsが入っている環境なら
npm install
npm start
で起動します。
デフォルト設定だとlocalhost:8000でシステムにアクセス可能です。
windows用の起動batも入っています。
3.2 ディレクトリ構成
- app (バックエンド用プログラムroot)
- modules (ActiveModuleFramework用拡張モジュール置き場)
- db (ローカルDB用ファイル置き場)
- local_modules (ローカルモジュール置き場)
- active-module-framework (ActiveModuleFramework本体)
- public (フロントエンド公開用ディレクトリ)
- css (スタイルシート置き場)
- js (TypeScript出力結果)
- public_src (フロントエンド用ソースコード)
- jsw (JSWライブラリ本体)
- main (フロントエンド用プログラム)
- scss (フロントエンド用CSS)
- sock (UNIXドメインソケット使用時のファイル置き場)
- template (トップページHTML置き場)
フレームワークを利用したアプリケーション用コードはappの中に書いていきます。基本的にはAmfModuleというクラスを継承したクラスをapp/modulesディレクトリに追加していく形です。ファイルがあったら自動ロードする方式なので、モジュールの登録のために別の場所を書き換える必要はありません。クラスの入ったファイルを作ればすぐに使えます。
フロントエンド側のコードに関して、WebPackなどのモジュールバンドラは使用していません。TypeScriptとscssのコンパイラがあれば十分です。public/cssやpublic/jsに入っているファイルは、HTMLの<HEAD>~</HEAD>で読み出し用のコードを自動生成、さらにレスポンスヘッダにlinkを追加して、HTTP2のサーバプッシュもついでにやります。この機能はhttps化が必要です。
ディレクトリ構成は、初期パラメータで変更できるようにしてあります。
import * as amf from 'active-module-framework/Manager'
const params : amf.ManagerParams = {
remotePath: '/', //一般コンテンツのリモートパス
execPath: '/', //コマンド実行用リモートパス
rootPath: 'public', //一般コンテンツのローカルパス
cssPath: ['css'], //自動ロード用CSSパス
jsPath: ['js'], //一般コンテンツのローカルパス
localDBPath: 'db/app.db', //ローカルDBパス
modulePath: 'app/modules', //モジュール配置パス
jsPriority: ['jsw.js'], //優先JSファイル設定
debug: true, //デバッグ用メッセージ出力
listen: 8000 //受付ポート/UNIXドメインソケット
//listen:'sock/app.sock'
}
new amf.Manager(params)
3.3 バックエンドモジュールとフロントエンドの通信
以下がバックエンド側を抜粋したものです。メソッドにJS_が付いているものは、フロントエンドと通信するのに使われます。
async JS_addUser(id: string, pass: string):Promise<boolean> {
const users = await this.getModule(Users) //ユーザモジュールを取得
if (!users.isAdmin()) //管理者権限を持つか確認
return false
const reader = new MyDNSReader() //MyDNS情報取得用クラスのインスタンスを取得
if (!await reader.getSession(id, pass)) //MyDNSにログイン
return false
const info = await reader.getInfo() //MyDNSから情報を取得
if (!info)
return false
const localDB = AmfModule.getLocalDB() //ローカルデータベースを取得
//結果を書き込む
const result = await localDB.run('replace into mydns values(?,?,?)', id, pass, JSON.stringify(info))
return result.changes > 0
}
以下はフロントエンド側のコードです。addUserの呼び出しはクラス名とメソッド名、パラメータを指定しています。細かい部分はフレームワークがやるので、これだけでバックエンドの機能を呼び出すことが出来ます。awaitを用いているので、コールバック地獄は存在しません。しかもTypeScriptのトランスコンパイルとpromisejsの使用を前提とすれば、IE11やGoogleBotも解釈可能です。
//ユーザの追加要求
const info = await adapter.exec('MyDNS.addUser',textUserID.getText(), textUserPass.getText())
if (info) {
resolv(info) //戻ってきたユーザ情報を通知
msgLabel.setText('追加成功') //メッセージボックスに結果を表示
await JSW.Sleep(500) //ウエイト
this.close() //メッセージボックスを閉じる
}
else
msgLabel.setText('追加失敗') //エラーメッセージを表示
フロントエンド側の通信機能はJSW.Adapterという名前で実装しました。以下の機能があります。
- クラス.メソッド名、パラメータでのデータ受け渡し
- バックエンド側から送られてきたデータの変換
- 非同期のPromise化
- 同じタイミングで発生した他の通信要求を自動的に一つにまとめる
- execのパラメータを配列にすると、複数処理を記述できる
- セッション用ハッシュの管理
Promise化してあるので、非同期を順次処理のコードで実装可能です。
4 どこまで簡単に出来るか
主目的はSPAのプログラムをとことん簡単に作ることにあります。「楽をするためなら、苦労は厭わない」それがプログラマの基本姿勢だと思っています。本来なら既存のフレームワークを使えば良いのですが、自分がやりたいこととのギャップが大きくなり、気がつくと結局自作しています。いつもだいたいこの流れです。
- 既存のフレームワークを使う
- こうすればもっと楽になる
- これをやるんだったらこの機能がいらないし逆に邪魔
- だったら根本的に自分で作った方が楽
ということで、これからも楽をする方法を苦労してでも追求していこうと思います。