FileMaker Web ビューアとJavaScriptアドオンを分析
はじめに
Webビューアを使って本格的なWebアプリケーションをFileMakerに組み込む流れがver 19.1から強くなってきた。
あくまでレイアウトオブジェクトだったWebビューアが、スクリプトとの連携が行えるようになり、新しいFileMaker上のスクリプトエンジンとしての側面も出てきた。
これから本格的に利用するにあたって、細かいメモなどをここに残してみるテスト。
※WebビューアはmacOS, Windows でエンジンが変わるため、今回はmacOSのみでの調査とする。
※Linux(Serverのみ) もあるが、サーバーサイド実行ではWebビューアは使えない。
Web Inspector
これがないとデバッグが難しい。
terminal で下記を実行し、フラグを立てたあとFileMakerを起動すると、
WebビューアのWebインスペクターが開けるようになる(Webビューアを右クリック)。
$ defaults write com.FileMaker.client.pro12 WebKitDebugDeveloperExtrasEnabled -bool YES
追記: FileMaker 19, 20 ではfilemakerが小文字になっている
$ defaults write com.filemaker.client.pro12 WebKitDebugDeveloperExtrasEnabled -bool YES
via https://blog.beezwax.net/2015/07/20/enable-debugger-for-a-filemaker-web-viewer/
Webビューア(JavaScript)からのFileMakerスクリプト呼び出しの正体
Claris FileMaker Pro ver 19.2.1.14(11-17-2020) での検証
Webビューアの「JavaScript による FileMaker スクリプトの実行を許可」オプションの正体はなんなのか。
オプションを有効にすると window.FileMaker
という変数が有効になり、FileMaker.PerformScript
および FileMaker.PerformScriptWithOption
が使えるようになる(ブラウザ上のグローバルな変数は window
にぶら下がってる)。
例えば、FileMaker.PerformScriptWithOption
Webインスペクタのコンソールを使えば簡単にソースコードが出てくる。
function (name, parameter, option) {
if (parameter == null) {
parameter = ""
}
if (option == null) {
option = "0"
}
var message = '{"command": "PerformScript", "value": { "name": ' + quote(name) + ', "parameter": ' + quote(parameter) + ', "option": ' + quote(option) + '}}';
// For mac
if (window.webkit && window.webkit.messageHandlers.fm != null) {
webkit.messageHandlers.fm.postMessage(message);
} else if (window.external != null) {
// For windows
window.external.onMessage(message);
}
// window.external.someCall(message);
}
macOS の場合は webkit.messageHandlers.fm.postMessage
、Windowsの場合はwindow.external.onMessage
を使ってFileMakerに引数を渡していることがわかる。
コメントアウトされているが、sameCall
と他の環境での対応を予定しているかのような書き方がされているということは、将来的にLinuxサーバーサイドで動くようになったりするかも?
PerformScript の中身は FileMaker.PerformScriptWithOption
の呼び出しのみになっていた。
function (name, parameter) {
FileMaker.PerformScriptWithOption(name, parameter, "0");
}
ちなみにこれ以降はネイティブコードのようなので、調べることはできなさそう。
webkit.messageHandlers
でググると WKWebView
を使ったSwiftとWebビューのデータのやり取りの方法などの記事が出てくるので、ちょっとみておくと面白いかも。
WebビューアのFileMakerオブジェクトの正体
FileMaker.PerformScriptWithOption
の中で出てくる quote
という関数が何者なのか気になったので調べようとしたらソースコードまるまる出てきた(この辺りのデバッグ能力がまだまだ)。
//
// WebScripting.js
//
// Created by Yun Jia on 3/28/19.
// Copyright © 2019 FileMaker, Inc. All rights reserved.
(function () {
if (window.FileMaker != null) return
window.FileMaker = {};
/// Following code comes from https://github.com/douglascrockford/JSON-js/blob/master/json2.js
var meta = { // table of character substitutions
"\b": "\\b",
"\t": "\\t",
"\n": "\\n",
"\f": "\\f",
"\r": "\\r",
"\"": "\\\"",
"\\": "\\\\"
};
var escapableExp = /[\\"\u0000-\u001f\u007f-\u009f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g;
function quote(string) {
// If the string contains no control characters, no quote characters, and no
// backslash characters, then we can safely slap some quotes around it.
// Otherwise we must also replace the offending characters with safe escape
// sequences.
escapableExp.lastIndex = 0;
return escapableExp.test(string)
? "\"" + string.replace(escapableExp, function (a) {
var c = meta[a];
return typeof c === "string"
? c
: "\\u" + ("0000" + a.charCodeAt(0).toString(16)).slice(-4);
}) + "\""
: "\"" + string + "\"";
}
/// Attribute end.
FileMaker.PerformScriptWithOption = function (name, parameter, option) {
if (parameter == null) {
parameter = ""
}
if (option == null) {
option = "0"
}
var message = '{"command": "PerformScript", "value": { "name": ' + quote(name) + ', "parameter": ' + quote(parameter) + ', "option": ' + quote(option) + '}}';
// For mac
if (window.webkit && window.webkit.messageHandlers.fm != null) {
webkit.messageHandlers.fm.postMessage(message);
} else if (window.external != null) {
// For windows
window.external.onMessage(message);
}
// window.external.someCall(message);
}
FileMaker.PerformScript = function (name, parameter) {
FileMaker.PerformScriptWithOption(name, parameter, "0");
}
})()
FileMaker から WebビューアでJavaScript を実行する
Web ビューアで JavaScript を実行
スクリプトステップのドキュメントを見ると、予め用意されているグローバルな関数を実行するためのインターフェイスに見えるが、実際はスクリプトステップの内容をevalしているような実装らしく、下記のように書くと任意のコードを実行可能。
# move google.com
Web ビューアで JavaScript を実行 [ オブジェクト名: "webview" ; 関数名: "(function(){ window.location.href = \"https://google.com\" })" ]
引数の中身だけ
(function(){ window.location.href = "https://google.com" })
要するに関数名の中身を関数そのものにすることで、Webビューア上で未定義の関数が実行できる。
自前のコードをWebビューアで表示している場合は不要だが、任意のWebサイトで任意のJavaScriptを実行したい時にこの方法が有効。
例えば
特定のサイトへの自動ログインを実装したい
(function(){
document.getElementById("username").value = "user";
document.getElementById("password").value = "password";
document.getElementById("loginButton").submit();
})
現在表示しているページの任意の値
(function(){
target = document.querySelector("h1").innerText;
FileMaker.PerformScriptWithOption("h1 を保存", target, "0");
})
など。
Webビューアで FileMaker.PerformScript が動かない
FileMaker.PerformScript
がWebビューアで動かないことがあるようだが、これはバージョンの問題かと思われる。
if we try this in WebDirect, the app will load, but the FileMaker.PerformScript function is not injected into the web viewer code so we have no way of pulling or pushing info between the FileMaker file and the web viewer. In WebDirect the web viewer reference must have the data:text/html prefix in order for the FileMaker.PerformScript function to be injected. In order to get this to work in WebDirect, a data URL is used for the actual page, so the FileMaker.PerformScript function is injected properly, and then include an iFrame that references the hosted app within the data URL. Here’s an example
via https://www.seedcode.com/filemaker-19-hosted-webviewer-add-on/
Webビューアが"data:text/html,"から始まらない "http(s)://" 形式の場合 FileMaker.PerformScript
が読み込まれず、スクリプトが実行できないいので、"data:text/html" で初め、iframeで自前でホストしてるwebサービスを利用しようと書いてあるが、今はWebビューアのオプション「JavaScript による FileMaker スクリプトの実行を許可」を有効にすることで、どこでも window.FileMaker
が注入される様子。
http://google.com/
を開いたWebビューアで window.FileMaker
にオブジェクトあることを確認
いつから使えるのかわからないが、FileMaker Pro 19.2.1 で確認できた。
WebビューアからFileMakerスクリプトを実行するもう一つの方法
最近は window.FileMaker
がいるので忘れがちだが、URLスキームを利用する方法がある。
少し古いFileMakerでも実行でき、URLなので扱いやすい場合があるかもしれない。
fmp://$/FILENAME.fmp12?script=SCRIPTNAME¶m=PARAM
公式: URL を使用してファイルを開く
以前はこれを使って実際にWebビューアアプリを作ってた。
param に JSONを渡してFileMakerスクリプトでインポートするみたな感じのものを作っていたが、URLの文字数制限などに引っかかることもなく動いていたので、それなりのデータのやり取りは問題なさそう(限界を試してはない)。
注意点として、拡張アクセス権の mfurlscript
が必要となる。
JavaScript アドオンはどうやって動いてるのか (1)
Webビューアを使ったアドオンが標準機能でついて、FileMaker では実装が難しいカレンダーやタイマーなどの機能が簡単に使えるようになりました。
あやつらがどうやって動いているのかを覗いていきます。
タイマー
とりあえず、サンプルとして小さそうなアプリ「タイマー」をみていきます。
「タイマー」のWebビューアの設定
Webアドレスの内容はこんな感じ(汚いインデントなどは修正してます)。
Let([
AddonUUID = "97DF59C9-D9D6-4C6A-AFD8-9D69398C12E1";
Config = TimerConfig(AddonUUID);
initialProps = JSONSetElement ("";
["Config"; Config; JSONObject];
["AddonUUID"; AddonUUID; JSONString];
// ---Optional---
["Meta.System.Appearance" ; Get(システムの外観) ; JSONString];
["Meta.System.Platform" ; Get(システムプラットフォーム) ; JSONString];
["Meta.Application.Version" ; Get(アプリケーションバージョン) ; JSONString];
["Meta.Application.Language" ; Get(アプリケーション言語) ; JSONString];
["Other.message"; "I am an initialProp" ; JSONString]
)
];
TimerDataURL(initialProps)
)
TimerDataURL
というカスタム関数に initialProps
という名の引数を渡してますね。
カスタム関数の中身はこんな感じ(インデント直してます
/**
*
* This function is responsible for building the Addon's Data URL
* and merging with the initialProps.
*
* @param {object} intialProps = this is the object thaat will get merged into the HTML
*
* @returns {string} the data url
*
*/
Let([
// this is the field that holds the Addon HTML
fName = GetFieldName(Timer::HTML);
url = If(
// if we are in DEV MODE just return the Dev URL
not IsEmpty($$Timer_DEV_URL) ; $$Timer_DEV_URL ;
// fetch the HTML and merge it
Let([
//this forms the SQL statement protecting against name changes
split = Substitute(fName; "::"; "¶");
t = Quote(GetValue(split; 1));
f = Quote(GetValue(split; 2));
sql = "SELECT " & t & "." & f & " FROM " & t & " FETCH FIRST 1 ROWS ONLY";
html = "data:text/html," & ExecuteSQL(sql; ""; "");
html = Substitute(html; ["\"__PROPS__\""; IntialProps])
];
html
)
)
];
url
)
細かい部分は省きます。ポイントは2つ
-
$$Timer_DEV_URL
開発用の変数。
みたままですが、Webアプリ開発時localに立てたWebサーバーを直接FileMakerで表示させるためのオプション。修正するたびにFileMakerにHTML読み込ませるのが面倒なので、$$Timer_DEV_URL
にhttp://localhost:3000
などをセットして開発しているものと思われます。 -
変数
html
はSQLでTimer::HTML
を参照してます(そうすることでどのレイアウトにいてもHTMLソースが取得できる)。Timer::HTML
テキストフィールドにはHTMLソースがそのまま入ってます。
そのHTMLの"__PROPS__"
という文字列をInitialProps
変数で置換することで、引数を渡してます。
JavaScript アドオンはどうやって動いてるのか (2)
"__PROPS__"
が実際どうやって読み込まれてるのか。
babel でコンパイルされたコード(多分)なのでみづらいですが、"__PROPS__"
で置換されるコードはこんな感じになってました。
function i(e) {
var t = arguments.length > 1 && void 0 !== arguments[1] ? arguments[1] : null,
n = arguments.length > 2 && void 0 !== arguments[2] ? arguments[2] : "false";
if (window.__initialProps__ = "__PROPS__", t) return e(t);
if ("__PROPS__" !== window.__initialProps__) {
try {
window.__initialProps__ = JSON.parse(window.__initialProps__)
} catch (o) {}
window.__initialProps__.webDirectRefresh = n;
var r = setInterval((function () {
window.FileMaker && (clearInterval(r), e(window.__initialProps__))
}), 100)
} else window.loadInitialProps = function (t) {
try {
t = JSON.parse(t)
} catch (o) {}
t.webDirectRefresh = n, window.__initialProps__ = t, e(t)
}
}
とりあえず渡した引数 InitialProps
は window.__initialProps__
に行くようです。
引数の e
の中身はWebインスペクターで見たところこんな関数でした。
function le(e){n.a.render(l.a.createElement(re,e),document.getElementById("root"))}
多分 ReactDom で <div id="root"></div>
に Reactコンポーネントを展開する関数ですね。
要するにReact実行前に引数(window.__initialProps__
)をチェックして、問題なければReactを展開するということだと思います。
多分要約するとこんなイメージ
ReactDOM.render(<Timer {...window.__intialProps__} />, document.getElementById('root'));
このJavaScript内の t
の正体はちょっとわかりませんでした。
InitialProps を上書きするためのコードだと思われるのですが、今回の「タイマー」では使われてる形跡がありませんでした。
tはデバッグ用の模擬データ注入用の変数かも。
JavaScript アドオンはどうやって動いてるのか (3)
大体の動き始めがわかった。
ではあのHTMLはどうやって作られているのかが問題になる。
最近のWebフロント関連の技術を利用して作っているのは明らかで、構成としては下記のような感じだと思われる。
- node.js を利用したコマンドライン環境での開発
- webpackを使った複数ファイルの統合や変換処理
- 主要なライブラリはReactを使用(多分)
- Bootstrap 4
node.js
node.js は JavaScript を実行するコマンドラインツール。
JavaScript はブラウザで動くものではなく結構前からサーバーなどでも利用されており、HTML, CSS, JavaScript 関連のツールやライブラリーの多くは node.js を利用して開発されている。
webpack
webpackはnode.jsで実行、利用するツール。
ライブラリーなどを読み込み、自前のツールと統合して1つのJavaScriptに書き出したりすることが主要な機能。
旧来、HTMLで<link>
タグを使ってjQueryをインポートしていたようなことを不要にして、1つにまとめたJavaScriptに書き出しすイメージ。
webpackにはプラグイン(モジュール)を利用して他にも色々な機能をつけることができるため、扱えるデータはJavaScriptだけでなくCSS, 画像などブラウザで扱う要素は大体扱えるようになっている。
例えば、最新のブラウザでしか動かないような書き方をしたJavaScript, CSSをwebpackを通じて変換することでIE 11でも問題なく動くようにしたりといったことが可能。
FileMakerで大きいのは、簡単に1つのHTMLファイルにJavaScript, CSS, 画像を埋め込んだ形で書き出したりすることもできるということ。
ただし、webpack周りのプラグインは無限にあり、無限に色々なことができるのでベストプラクティスを見つけるのが難しいし、プラグインを多数組み合わせて使うため思い通りに動かない時何が原因なのか分からず無限に時間を消費することがある。
React
Web UIを作るときによく使われるライブラリ。
JSXというJavaScriptとHTMLを統合したような言語で記述でき、色々な制約がある代わりに簡単に複雑なUIが実装できる。公式のチュートリアルがすごく丁寧でわかりやすいので、それを読むだけである程度すぐに使えるようになる。
JavaScriptアドオンで使われているのがReactであるかどうかは確かではないが、これ系のライブラリを使っていると思われる。
Bootstrap 4
言わずと知れた有名なCSSフレームワーク。
Bootstrap 4.0 beta がなぜか使われている様子。2017年ごろから作り始めてたのかな?
基本的にはclassをHTMLタグに指定するだけでレイアウトが作れる。細かいこだわりがないのであればとりあえずこれ使っておけばそれなりのUIが作れる。CSSの知識がない人がとりあえず使うのにもぴったりかも。
今は Bootstrap 5 betaが出ていて、今から作るのであれば5を使ったほうが使いやすいと思いう。
JavaScript アドオンはどうやって動いてるのか (4)
JavaScriptアドオンのソースコードはそのまま見ることができる[1]が、あれを改造して使うのはかなり難しい。
あれは直接人が書いたコードではなく、webpackによってビルドされ色々な加工が施されたもので、直接人が読んだり、改造したりできるような代物ではない。
なぜ、そんなことになるのかというと、別に読みづらくするために、改造されないようにそうしているわけではなく、先にあげたようなツールを使うと自然とそういったものが出来上がるというだけ。
主にwebpackを経由してどういったことが行われているかというと下記のようなことが行われている。
- 複数のJavaScriptファイル、ライブラリなどをまとめて1つ(または任意の)ファイルに書き出してくれる
- css-loader を使ってCSSもJavaScript内にまとめてくれる
- Babel を使って新しいブラウザでしか動かないようなJavaScriptの書き方をしてもIE 11でも動いてくれるように変換してくれる
- html-webpack-inline-source-plugin を使ってまとめた CSS, JavaScript を HTMLに展開して1つのHTMLファイルにまとめてくれる
JavaScriptアドオンの作成のキモは html-webpack-inline-source-plugin。
1つのHTMLファイルにまとめられることで、専用のWebサーバーもいらないし、オフライン環境でも実行できるWebアプリが作れる。これだけのためにwebpackを使った開発環境を整える価値があるとも言える。
また、IE 11に対応したJavaScriptを手で書くのは今の時代かなり辛い。Babel系のツールを使うことで、その大半が解決できる(全てじゃないけど…)。
具体的な方法などは他にもたくさん情報があるので、ここにあげた名前を手がかりに探してみてほしい。
-
タイマーの場合は
Timer::HTML
のフィールドに入っている。 ↩︎
実際のReact App作成については記事にしました。