Closed14

FileMaker Web ビューアとJavaScriptアドオンを分析

HAZIHAZI

はじめに

Webビューアを使って本格的なWebアプリケーションをFileMakerに組み込む流れがver 19.1から強くなってきた。

あくまでレイアウトオブジェクトだったWebビューアが、スクリプトとの連携が行えるようになり、新しいFileMaker上のスクリプトエンジンとしての側面も出てきた。

これから本格的に利用するにあたって、細かいメモなどをここに残してみるテスト。

※WebビューアはmacOS, Windows でエンジンが変わるため、今回はmacOSのみでの調査とする。
※Linux(Serverのみ) もあるが、サーバーサイド実行ではWebビューアは使えない。

HAZIHAZI

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/

HAZIHAZI

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インスペクタのコンソールを使えば簡単にソースコードが出てくる。

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);
  }

macOS の場合は webkit.messageHandlers.fm.postMessage、Windowsの場合はwindow.external.onMessage を使ってFileMakerに引数を渡していることがわかる。
コメントアウトされているが、sameCall と他の環境での対応を予定しているかのような書き方がされているということは、将来的にLinuxサーバーサイドで動くようになったりするかも?

PerformScript の中身は FileMaker.PerformScriptWithOption の呼び出しのみになっていた。

FileMaker.PerformScript
function (name, parameter) {
  FileMaker.PerformScriptWithOption(name, parameter, "0");
}

ちなみにこれ以降はネイティブコードのようなので、調べることはできなさそう。

HAZIHAZI

webkit.messageHandlers でググると WKWebView を使ったSwiftとWebビューのデータのやり取りの方法などの記事が出てくるので、ちょっとみておくと面白いかも。

HAZIHAZI

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");
  }

})()
HAZIHAZI

FileMaker から WebビューアでJavaScript を実行する

Web ビューアで JavaScript を実行 スクリプトステップのドキュメントを見ると、予め用意されているグローバルな関数を実行するためのインターフェイスに見えるが、実際はスクリプトステップの内容をevalしているような実装らしく、下記のように書くと任意のコードを実行可能。

FileMaker Script
# 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");
})

など。

HAZIHAZI

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 で確認できた。

HAZIHAZI

WebビューアからFileMakerスクリプトを実行するもう一つの方法

最近は window.FileMaker がいるので忘れがちだが、URLスキームを利用する方法がある。
少し古いFileMakerでも実行でき、URLなので扱いやすい場合があるかもしれない。

fmp://$/FILENAME.fmp12?script=SCRIPTNAME&param=PARAM

公式: URL を使用してファイルを開く

以前はこれを使って実際にWebビューアアプリを作ってた。
param に JSONを渡してFileMakerスクリプトでインポートするみたな感じのものを作っていたが、URLの文字数制限などに引っかかることもなく動いていたので、それなりのデータのやり取りは問題なさそう(限界を試してはない)。

注意点として、拡張アクセス権の mfurlscript が必要となる。

HAZIHAZI

JavaScript アドオンはどうやって動いてるのか (1)

Webビューアを使ったアドオンが標準機能でついて、FileMaker では実装が難しいカレンダーやタイマーなどの機能が簡単に使えるようになりました。

あやつらがどうやって動いているのかを覗いていきます。

タイマー

とりあえず、サンプルとして小さそうなアプリ「タイマー」をみていきます。


「タイマー」のWebビューアの設定

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 という名の引数を渡してますね。

カスタム関数の中身はこんな感じ(インデント直してます

TimerDataURL
/**
*
* 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つ

  1. $$Timer_DEV_URL 開発用の変数。
    みたままですが、Webアプリ開発時localに立てたWebサーバーを直接FileMakerで表示させるためのオプション。修正するたびにFileMakerにHTML読み込ませるのが面倒なので、$$Timer_DEV_URLhttp://localhost:3000 などをセットして開発しているものと思われます。

  2. 変数 html はSQLで Timer::HTML を参照してます(そうすることでどのレイアウトにいてもHTMLソースが取得できる)。Timer::HTML テキストフィールドにはHTMLソースがそのまま入ってます。
    そのHTMLの"__PROPS__" という文字列を InitialProps 変数で置換することで、引数を渡してます。

HAZIHAZI

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)
  }
}

とりあえず渡した引数 InitialPropswindow.__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 を上書きするためのコードだと思われるのですが、今回の「タイマー」では使われてる形跡がありませんでした。

HAZIHAZI

tはデバッグ用の模擬データ注入用の変数かも。

HAZIHAZI

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を使ったほうが使いやすいと思いう。

HAZIHAZI

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系のツールを使うことで、その大半が解決できる(全てじゃないけど…)。

具体的な方法などは他にもたくさん情報があるので、ここにあげた名前を手がかりに探してみてほしい。

脚注
  1. タイマーの場合は Timer::HTML のフィールドに入っている。 ↩︎

このスクラップは2021/02/28にクローズされました