Dinosaur7 は純粋なHTMLとJavascriptでSPA(Single Page Application)を構築できる軽量フロントエンドフレームワークで、コンパイル&パッケージングは不要、サードパーティ・ライブラリも特に必要ありません。
また、バックエンドとの連携も容易に行えます。
これからも Dinosaur7 をどうぞよろしくお願いします。
Dinosaur7 の名前ですが、作者の子供が7歳のごろから恐竜が大好きだったのが由来です。
一言でいうと、従来SSR(Server Side Rendering)型プロジェクトのファイル構成とほぼ同じで良いです。
他のSPAシステムと同じですが、サーバー側はちょっとしたProxy設定(またはResource handle)が必要となります。
HttpRequestに対し以下の振り分けを行うようにしてください。
/**/*.* - # 拡張子があるものはそのまま転送する
/api/** - # API(BackendService)の呼び出しはそのまま転送する
/** /index.html # 上記以外は全部/index.htmlを返す
※ご存じかと思いますが、上記 /api は固定というわけではありません。
テスト用のJava製HttpServerである DevServer はHomeからダウンロードできます。
中にはDinosaur7サンプルも含まれていて、解凍したら中の実行ファイルを実行(※)するだけで起動されるはずです。
起動されたらデフォルトの http://localhost:8080 でアクセスください。
DevServerには、APIMock機能が実装されており、APIフォルダにYAMLファイルを置くことでほしいHttpResponseが実現できます。
DevServerはsourceもダウンロード可能ですので、中身が気になる方はどうぞ自由にご覧ください。
※実行するにはjre8以上の環境が必要です。
Applicationとは Dinosaur7 フロントエンドアプリケーションを指し、変数名appで全アプリケーションスコープからアクセス可能です。まず、以下のApplication入り口である/index.htmlの例から理解を進めましょう。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Dragonfly5</title>
<link rel="stylesheet" type="text/css" href="/mod/dinosaur7/dinosaur7.v3.css" />
<script type="application/javascript" src="/mod/dinosaur7/dinosaur7.v3.1.js"></script>
</head>
<body d7app style="display:none;">Here to locate page</body>
<script type="application/javascript">
app.setting({
baseApiUri: '/api/dev',
baseHtmlUri: '/html/dragonfly5',
defaultLayout: 'defaultLayout',
defaultExtension: 'html',
showProgress: false,
enableHistBack: false,
topPage: 'overview',
route: [
{name:'defaultLayout', path:'/develop/layout.html', preload:true},
{name:'overview', path:'/home/overview.html', preload:true},
{name:'messageBox', path:'/develop/common/messageBox.html'},
]
});
app.setRouteInterceptor(async (nameOrPath, query)=>{ //ページ遷移前処理
// ログイン状態判定し、ログイン画面に飛ばすとか
return true;
});
app.api.setRequestInterceptor((method, url, query, header)=>{ // API呼出前処理
// headerにtoken設定するとか
return {url, query, header};
});
app.api.setResponseInterceptor((method, url, query, responseData, request)=>{ // API呼出後処理
// 共通エラーコード処理とか
return responseData;
});
</script>
</html>
app.setting 初期化パラメータを設定します。詳細は以下の通りです。
baseApiUri APIのURIところのPrefixを指定します。バックエンド呼び出す際にすこし楽になるだけで、必須ではありません。
baseHtmlUri HTMLのURIところのPrefixを指定します。Page遷移やComponentロード時にすこし楽になるだけで、必須ではありません。
defaultLayout PageのHTML内にLayoutを指定しなかった場合、適応されます。詳細は後続のPageにて説明します。必須ではありません。
defaultExtension Componentファイルの拡張子を指定します。デフォルトはhtmlになります。
showProgress Http通信を行う際、動作中を示すぐるぐる回るアイコンが表示されます。デフォルトでは表示されません。必須ではありません。
enableHistBack ブラウザの「戻る」「進む」ボタン機能を有効化します。デフォルトでは機能しません。必須ではありません。
topPage ドメインのみを指定したり、存在しないURLで画面を開いたりした場合にリダイレクトされる画面を指定します。ここは指定した方が良いでしょう。
route Componentファイルに名前を付けることで、ページ遷移処理呼び出しで、URL以外に名前指定ができるようになります。
preload はイベント等によるページ遷移処理時ではなく、app初期段階で事前にロードして置くようにします。必須ではありません。
app.setRouteInterceptor app.route関数で画面遷移を行う際に呼ばれるカスタム関数を設定します。
カスタム関数 async (nameOrPath:string, query:map<key:string, value:any>):boolean 戻り値がtrue以外の場合は遷移処理が中断されます。当該カスタム関数設定は必須ではないが、ログインしたかの判定を行い、ログイン画面に飛ばしたりする処理が記載できます。
app.api.setRequestInterceptor app.api系関数を呼び出す際に呼ばれるカスタム関数を設定します。
カスタム関数 (method, url, query, header):{url, query, header} の設定は必須ではないが、headerにtoken設定を行うとかの共通処理が記載できます。
app.api.setResponseInterceptor app.api系関数を呼び出してからのresponseに対して共通処理を行うためのカスタム関数を設定します。
同じく、カスタム関数 (method, url, query, responseData, request):any は必須ではないが、エラーコードによるメッセージ表示などの共通処理が可能です。
app.apiカスタム関数に渡されるパラメータの説明は以下の通りです。
method:string app.api系関数を呼び出す際のGET, POST, PUT, DELETEです。
url:string app.api系関数を呼び出す際のurlそのままです。
query:map<key:string, value:any> app.api系関数を呼び出す際のqueryそのままです。
header:map<key:string, value:any> app.api系関数を呼び出す際のheaderそのままです。
responseData:map<key:string, value:any> HttpResponseを標準化したデータセット、下の API呼出 をご参考ください。
request:XMLHttpRequest API呼び出し用XMLHttpRequest本体です。
上記以外にも app には以下の機能が備えています。
async app.route(nameOrUrl:string, query:map<key:string, value:any>):void ページを遷移させます。上記app.settingにて設定済みの場合はnameOrUrlにはurl以外にnameが指定できます。
app.currentPath():string 今のPathを返します。
app.currentQuery():map<key:string, value:any> 今のQueryを連想配列として返します。
app.storeData(key:string, value:any) 値をlocalStorageまたはcookieに保存します。
app.storeData = (key:string, value:any) で保存します。
app.storeData = (key:string) で取得します。
Pageとは、SSRレガシーシステムでのページ(JSP, PHPなど)とほとんど同じイメージで良いです。
<!--
GroupIdを引数に、ユーザー一覧を表示する.
nameをクリックすると、詳細画面に遷移する。
-->
<style> <!-- Component scope -->
.groupName {
font-size: 24px;
color: red;
}
</style>
<div>GroupName: <span class="groupName">{% _m.groupName %}</span></div>
<table>
<thead>
<tr>
<th>No.</th>
<th>id</th>
<th>name</th>
<th>status</th>
</tr>
</thead>
<tbody>
<tr d7="for(var idx in _m.users) const row = _m.users[idx];">
<td>{% idx %}</td>
<td>{% row.id %}</td>
<td onclick="clickUser(row.id)">{% row.name %}</td>
<td d7="if(row.status === 'OK')"><img src="/images/good.png"></td>
<td d7="if(row.status === 'NG') console.log('arara');">-_-</td>
</tr>
</tbody>
</table>
<div name="company" d7comp="/company/summary?param={% _m.companyId %}">locate company info here.</div>
<script>
_m.groupId = query.groupId;
// 初期処理
const init = async function() {
let response = await app.api.get(`/group/${query.groupId}`);
_m.companyId = response.data.companyId;
_m.groupName = response.data.groupName;
_m.users = response.data.users;
}
// 詳細画面に遷移
const clickUser = function(id) {
let query = {userId: id};
app.route('/user/detail.html', query);
}
</script>
裏で Dinosaur7 はこのページHTMLを解析&カプセル化し、Component として扱っています。
Pageのscriptからアクセスできるリソース変数は主にapp, query, me, _m があります。
app 上で紹介した Application そのものです。
query:map<key:string, value:any> ブラウザーのアドレスバーから、またはapp.route関数経由でページを開いた時のqueryパラメータとなります。
me ページ自身を指します。詳細は下 Component のところをご参考ください。
_m モデルデータを指します。このモデルデータよって画面がレンダリングされます。
Pageには主にHTML部とScript部がありますが、Script部で _m に対しデータ加工を行い、HTML部では _m を基にレンダリングする流れになります。習慣のことですが、HTML部とScript部の位置は逆でも構いません。
HTML部
{% value %} 値をレンダリングします。(SampleLine12)
d7 Attribute[d7=”if|for expression”]にはHtmlTag(ブロック)コントロールするためのifかfor文が書けます。(SampleLine23,27)
d7comp Attribute[d7comp=”/pathToComp.html”]は当該場所にロードするコンポネントを指定します。Attribute[name]を設定することで、コンポネントに名前を付けられます。(SampleLine33)
onXXXXX Eventアクションの定義は標準HTML仕様通り、但し、(;)付きのプロセスではなく、「呼び出し定義」のみを指定してください。下の Event関数 も一緒にご参考ください。(SampleLine26)
didrender Attribute[didrender=”process expression;”]は当該Elementがレンダリング(または再レンダリング)されたら、呼び出される処理を書きます。
d7part Attribute[d7part]の指定(値不要)は、サードパーティ・ライブラリとの組み合わせで必要になる場合が多いが、子供Elementが少しでも変わったら当該Element全体をレンダリングし直したい場合に指定します。殆どの場合、上記Attribute[didrender]とペアで指定することが多いでしょう。
Script部
純粋なJavascriptと比べ、書き方に特別な制限はありません。query.xxxでPageパラメータを受け取ったり、app.routeでPage遷移を行ったりすることができます。
強いて言えば、initという名前の関数定義(SampleLine38)は名前の通りinit関数が最初に実行され、その後初期画面のレンダリングが始まります。
init関数は Event関数 と同じ扱いですので、初期画面のレンダリングが終わってから何かしらの処理を行いたい場合は戻り値に関数を定義してください。
Layoutは部品の共通化を行うための便利な機能です。以下のようなPage定義があるとしましょう。
<!-- page.html -->
<div d7layout="/path/layout.html?param=xxx">use layout.html as layout template.</div>
<div>
<div class="blockA"></div>
<div class="blockB"></div>
</div>
<script>
//
</script>
お気付きかと思いますが、2行目のattribute[ d7layout ]には適応するレイアウト/path/layout.htmlが指定されています。
以下は/path/layout.htmlの中身です。5行目にd7pageが記されています。
<!-- layout.html -->
<div>
<div class="menu"></div>
<div class="sidemenu"></div>
<div class="contents" d7page>locate page here.</div>
</div>
<script>
//
</script>
これで上のPageの表示はLayout[/path/layout.html]に包まれた状態で表示されるようになります。そして、LayoutもPageと同じくComponentの一種ですので、Pageと変わらない機能が実装できます。
上で触れたように Dinosaur7 ではすべてのHTMLが Component としてカプセル化され、組み立てられます。PageもLayoutもコンポネントであれば、後ろに出てくるModalもコンポネントの一種に過ぎません。
自分自身は me、 親コンポネントは me.getParent()、 子供コンポネントは me.getComponent(idOrName) の方法でアクセスできます。
Component機能一覧
getId():string コンポネントIDを返します。
getName():string コンポネント名を返します。コンポネント名がなかった場合はIDを返します。
set(key:string, value:any):void コンポネント(me, parent, child)をハンドラに、値を受け渡しできるように保存します。
get(key:string):any コンポネントに set した値を取得します。
regist(funcName:string, func:function):void ほかのコンポネントから呼び出せるように機能を拡張します。
async invoke(funcName:string, …params:any):void registで拡張したコンポネント機能を呼び出します。
async reflect():void モデルデータに変更があった場合、画面に反映(レンダリング)を行います。
getParent():Component 親コンポネントを返します。
async loadComponent(nameOrPath:string, query:map<key:string, value:any>, locateSelector:string, compName:string):Component 子供コンポネントを指定場所にロードします。
removeComponent(idOrName:string):void 子供コンポネントを削除します。
getComponent(idOrName:string):Component 子供コンポネントを返します。
async modal(nameOrPath:string, autoClose:boolean):Component コンポネントをモーダルモードで開くためのハンドラを返します。下の Modalモード も一緒にご参考ください。
hide(selector:string):void コンポネント配下のElementを非表示します。selectorなしの場合は、コンポネント自体を非表示します。
show(selector:string):void コンポネント配下のElementを表示します。selectorなしの場合は、コンポネント自体を表示します。
s(selector:string, indexOrNone:int):Element 当該コンポネント配下のElementを返します。複数結果の場合、indexでターゲットを絞れます。
sAll(selector:string):list<Element> 当該コンポネント配下のヒットするすべてのElementを返します。
emitEvent(selector:string, eventName:string, val:any):void 当該コンポネント配下の指定Elementに対し、イベントを起こします。
すべてのComponentはModalモードで開けます。以下、使い方を示します。
<呼び出す側>
const modalA = await me.modal('/path/xxx.html');
const queryParams ={key: value};
const callBackWhenClose = async(returnValue)=>{
// do something here when close modal
});
modalA.open(queryParams, callBackWhenClose);
補足ですが、Modal画面外枠(overlay)をクリックすると閉じるようにするAutoClose機能を有効にしたい場合は、1行目でme.modal(path)の代わりにme.modal(path, true)にしてください。
<モーダル側>
const clickOK = () => {
me.close(returnValue); // モーダルを閉じる
}
const clickCancel = () => {
me.close(null); // モーダルを閉じる
}
ちなみに、デフォルトのModal画面効果がお好きではない場合は、dinosaur7.cssのd7_modal_xxx当たりを適当に弄ってください。
画面のonclickなどイベント処理やModalのcallback関数において、処理が終わった後、 Dinosaur7 はモデルデータ(_m)に変更があることを検知した場合、デフォルトでは画面への反映処理(reflect)を行います。
画面反映処理を制御したい場合は、以下のようにイベント処理のreturn値を工夫することで実現できます。
return false; モデルデータが変わっても画面反映処理は行いません。自分で画面反映処理を行いたい場合や、別のイベント処理で一気に反映させたい場合に使います。
return function(){ /*do something*/ }; 画面反映処理が終わった後、実行させたい関数処理を書きます。
Event関数やモーダルcallback関数は 非同期でも構いません、ただし、awaitなどを使って、最終return値が意図に合うように気を付けてください。
Dinosaur7 にはバックエンドを呼び出すためのAPIモジュールが用意されていて、定義は以下の通りです。
async app.api.get(url, query, header):map<key:string, value:any> 非同期GET メソッド
async app.api.post(url, data, header):map<key:string, value:any> 非同期POST メソッド
async app.api.put(url, data, header):map<key:string, value:any> 非同期PUT メソッド
async app.api.delete(url, query, header):map<key:string, value:any> 非同期DELETE メソッド
パラメータの説明は以下の通りです。
url:string pathまたはqueryParameter付きのurl指定
query:map<key:string, value:any> queryParametersを連想配列の形で指定
header:map<key:string, value:any> headerを連想配列の形で指定
data:any post, putするデータを指定
戻り値ですが、上記4つメソッドともresponse(XMLHttpRequest.responseText)を決まったフォーマットで連想配列として返却されます。
// httpResponseがjsonの場合
// responseTextをパースした結果をresponseとする。
{
_STATUS:string // response._STATUS存在しない場合追加、 HttpCode200時はOK、それ以外は WARN、通信異常の場合はERROR
_MSGCODE:string // response._MSGCODE存在しない場合追加、HttpCode200時は空、それ以外は 'HC' + HttpCode
_MESSAGE:string // response._MESSAGE存在しない場合追加、HttpCode200時は空、それ以外は response.message || response.error || ExceptionMessage
... // response本体
}
// httpResponseがjson以外の場合
{
_STATUS:string // HttpCode200時はOK、それ以外はWARN、通信異常の場合はERROR
_MSGCODE:string // HttpCode200時は空、それ以外は 'HC' + HttpCode
_MESSAGE:string // HttpCode200時は空、それ以外は 'Unknown.'
data:string // responseText
}
Utility d7util 変数名で、以下の便利機能が使えます。
d7util.inProgress(show:boolean):void 処理中(Loading)アイコン表示を制御します。d7util.inProgress()で表示、d7util.inProgress(false)で閉じます。
d7util.parseUrl(nameOrUrl:string):map<key:string, value:any> nameOrUrlを解析し、連想配列として返します。
{
path: ‘/xxx.html’,
query: {key: val},
origin: ‘https://hostname.com’,
name: ‘app.settingに定義がある場合のみ’
}
d7util.stringifyUrl(url:string, query:map<key:string, value:any>):string url文字列を生成します。
d7util.encodeHtml(strHtml:string):string HTML用に文字列をencodeします。
d7util.decodeHtml(strEncode:string):string encodeしたHTML文字列をdecodeします。
d7util.escapeReg(pattern:string):string 正規表現文字列をescapeします。
d7util.cloneVal(val:any):any データをdeepコピーします。
d7util.hasDiff(v1:any, v2:any):boolean mapまたはlistの値をdeep比較します。
d7util.decode(key, key1, val1, key2, val2, …):any keyValPair中からkeyと一致するパラメータ(keyN)の次の位置のパラメータ(valN)を返します。
d7util.or(val:any, val2:any):boolean valがundefinedの場合、val2を返します。
標準Elementのメソッド拡張
Dinosaur7は昨今のSPAフレームワークと違って、決してDOMを弄るな!ではなく、
すごく複雑な画面だったり、性能要件が極めて厳しかったりなどの場合は、いくら優れたフレームワークでも自動レンダリングには限界があるはずですので、
素直にハイブリット式で行きましょというのがDinosaur7のスタンスです。
もちろん選定したフレームワークの仕組みをよくご理解された上の話ですが、Dinosaur7は
1)過去レンダリング時点でのモデルデータと現時点でのモデルデータ(変数_m)の差分を基に画面をレンダリングする
2)Dinosaur7に自動レンダリングをさせたくない場合はアクション関数の戻り値をfalseにする だけのシンプルな動きをしています。
というわけでして、Dinosaur7ではHTMLDocumentの標準Elementに対して、使いやすいようにメソッド拡張を行っています。
例えば、me.s(‘.blockA’).s(‘#id’).hide();
s(selector:string, indexOrNone:int):Element Element.querySelectorと同じ機能だが、[d7id=”xxx”]はd7xxxに省略できたり、複数hitした場合、indexを指定することでターゲットを特定できます。
sAll(selector:string):list<Element> Element.querySelectorAllとほぼ同じ機能です。
hide():void 当該Elementを非表示させます。
show():void 当該Elementを表示させます。
setVal(val:string):void Attribute[value]が存在すればvalueに、それ以外はtextContentに値を設定します。
getVal():string Attribute[value]が存在すればvalueを、それ以外はtextContentを返します。
setAttr(prop:string, val):void Attributeを設定します。
getAttr(prop:string):string Attributeを返します。
rmvAttr(prop:string):void Attributeを削除します。
setStyle(propOrMap:string|map<key:string, value:string>, val:string):void styleのproperty追加を行います。
getStyle(propOrNone:string):string|map<key:string, value:string> styleのpropertyを取得します。指定なしの場合はstyle全体を連想配列として返します。
rmvStyle(…prop:string):void styleのpropertyを削除します。(複数指定可)
setClass(…clazz:string):void classを追加します。(複数指定可)
getClass(clazzOrNone:string):string|list<string> clazz存在すればclazz、それ以外はnullを返します。指定なしの場合はclass全体をリストとして返します。
rmvClass(…clazz:string):void classを削除します。(複数指定可)
特段なことしなくても、 Dinosaur7 に任せることで充分な性能を期待できるが、巨大な画面などでレンダリング性能をもっと上げたいときはピンポイントで描画対象を指定することができます。
方法1: HtmlEventにてselector指定
<div class=”button” onclick="clickOK()|#name|…">OK</div>
方法2: Event関数にてselector指定
Component.reflect(selectors:list<string>)
const clickOK = () => {
// some logic
me.reflect(['#name', ...]) // 描画(変更)対象指定
return false; // Dinosaur7の自動検知をやめさせる。
}
その他: d7id
TABLEのTRなどで、繰り返し回数が特に多い場合、Attribute[d7id]に値(注意! index値ではなく固有IDを指定してください)を与えることで、 Dinosaur7 の差分判断速度を上げることができます。
もう一点、d7idを与えた場合、Element要素検索のselectorの書き方がシンプルになる、例えば、通常ならme.s(‘[d7id=xxx]’)がme.s(‘d7xxx’)で済ませられます。
{[% logic script %]} HTML部で自由にロジックを書きたい場合の作法です。ただし、上記説明での d7=”if|for” では充分HTMLブロックの制御ができるはずですし、HTML部でロジックをべた書きするのはあまりお勧めしません。
<table>
<thead>
<tr>
<th>No.</th>
<th>id</th>
<th>name</th>
</tr>
</thead>
<tbody>
{[% for(var idx in _m.users) {
const row = _m.users[idx];
%]}
<tr>
<td>{% idx %}</td>
<td>{% row.id %}</td>
<td onclick="clickUser(row.id)">{% row.name %}</td>
</tr>
{[% } %]}
</tbody>
</table>
上で説明されたComponentは Dinosaur7 が解釈&カプセル化するためにシンプルなHTML構成になっているが、SEO対策等のため、どうしても Dinosaur7 経由ではなく、ブラウザーから直接静的ファイルをアクセスさせたいケースはあるでしょう。
Dinosaur7 ではこういうケースを想定して、以下の方法を提供しています。
<!DOCTYPE html>
<HTML>
<body>
<div>
<div>static parts<div>
</div>
<d7body style="display:none;" >
<div>Component html</div>
<d7script>
// Component script
</d7script>
</d7body>
</body>
</HTML>
そうです、完全なHTMLファイルの配置は可能です。中の Component 部分だけは d7body で囲み込んでください。そして、<script>部分はそのままだと実行されるので、 d7script に変えてください。
本題と直接関係はないが、 Dinosaur7 には d7dummy というAttributeも存在し、d7dummyが指定されているHtmlTag(ブロック)は省かされます。用途はデザイナーさんの用意してくれた綺麗なHTMLをコンポネント化する過程(コード埋め込み)で、もともとのパーツを残しつつ移植する場合などに役立つでしょう、恐らく。