JavaScriptコードの実行
このページの目的
前のページでは、ページの読み込みについて説明しました。このページでは、Rice経由でJavaScriptコードを実行する方法を学んでいきます。
目次:
ペインとブラウザ
Cooker は二ペインのアプリケーションです。このページのRiceコードは左側のペイン、Main-ブラウザ、で実行されると仮定されて実装されています。
右側のペインでも実行できますが、実行の結果はMain-ブラウザに現れます。
jsexecutorクラス
Cooker はウェブページを操作するためのソフトウエアであり、その操作は JavaScript を使うことで行います。
browser クラスは、ブラウザを操作する機能をまとめていました。同じように、JavaScript を操作する機能をまとめたクラスがあります。それが、jsexecutor クラスです。
jsexecutor クラスの機能の一例です。
任意の JavaScript コードを直接実行できます。
ウェブページ内に新しい script タグを埋め込めます。
ウェブページ内の JavaScript 関数を呼び出します。
JavaScript と Rice の間で文字列を受け渡しします。
JavaScript からのイベントを Rice で受け取ります。
以下のセクションでは、jsexecutor クラスの使い方について学びます。
Rice経由の直接実行
JavaScript の直接実行を説明するために前ページの example.cook を修正して、メッセージの表示を JavaScript で実行してみます。
以下に修正された example.cook を示します。5、18、20行目が追加、修正された部分です。
1: | class main |
2: | browser br; |
3: | switch sw; |
4: | message m; |
5: | jsexecutor jse; // フィールド宣言を追加。 |
6: | open method void start() |
7: | sw = br.LoadCompleted(this, "LCHandler(dictionary)"); |
8: | sw.Start(); |
9: | br.LoadUrl("https://www.example.com/"); |
10: | endmethod |
11: | |
12: | open method void end() |
13: | sw.Stop(); |
14: | endmethod |
15: | |
16: | open method void LCHandler(dictionary dic) |
17: | if(dic.IsSuccess) |
18: | jse.Execute("alert('ページロード成功。');"); // 直接実行。メッセージをJavaScriptを使って表示。 |
19: | else |
20: | jse.Execute("alert('ページロード失敗。');"); // 直接実行。メッセージをJavaScriptを使って表示。 |
21: | endif |
22: | endmethod |
23: | endclass |
五行目で jsexecutor クラスのフィールド、jse を宣言しています。
十八、二十行目では、jse 経由で jsexecutor クラスの Execute(string) メソッドを呼び出しています。
Execute(string) メソッドは引数を JavaScript コードとして実行します。
jsexecutorクラスの変数は定義なしで使うことが出来ます。その場合はMain-ブラウザのjsexecutorを表します。
上記コードを実行すると、ページロードが成功した後に JavaScript ダイアログが表示されます。
Execute メソッドのバリエーションを以下に示します。
1: | Execute(string) | 引数をJavaScriptコードとして実行します。即時関数化されます。 |
2: | Execute(string,bool) | 引数をJavaScriptコードとして実行します。即時関数化を選択できます。 |
3: | ExecuteFromFile(string) | 引数のファイルからをJavaScriptコードを読み込んで実行します。即時関数化されます。 |
4: | ExecuteFromFile(string,bool) | 引数のファイルからをJavaScriptコードを読み込んで実行します。即時関数化を選択できます。 |
即時関数化
上記の 1 と 3 は引数として与えられた JavaScript コードを強制的に即時関数化します。
上記の例の jse.Execute("alert('ページロード成功。');"); が実行されると、JavScript 環境に渡されるコードは以下の様になります。
即時関数や alert() 関数など、JavaScript 関連の用語や機能などについては説明を割愛します。
1: | (function(token){ |
2: | alert('ページロード成功。'); |
3: | }("0f8fbd59d9cb769fa16570867728950e")); |
1行目と3行目が引数に付加されて即時関数化されます。
1行目の引数のtokenと3行目のtokenの値については後述します。
上記の例ではalert() 関数を呼び出しているだけなので、即時関数化は必要ありませんが、JavaScript コードを即時関数として実行することには二つの利点があります。
一つ目の利点は、名前空間の汚染を防ぐことです。
関数定義内で var 文を使うと、その変数は関数スコープを持ちます。関数スコープの変数の参照範囲は関数の内に限定されますし、関数の終了と同時に破棄されます。
つまり、他の JavaScript コードに与える影響が最小になります。
関数定義外で var 文を使うと、その変数は大域スコープを持ちます。大域スコープの変数、大域変数は、ページが生きている間は、変数として残り続けます。
更に、同名の大域変数があった場合は、それを上書きしてしまいます。そうなったら、ページの正常な動作も不可能になります。
即時関数は、関数定義です。ですから、即時関数化を行えば名前空間の汚染を防ぐことができます。
二つ目の利点は、JavaScript からの返り値を受け取れることです。
Rice は、JavaScript 関数呼び出しの結果として、値を受け取ることが出来ます。
JavaScript 関数が return 文で値を返す場合は、それを Rice で受け取ることが出来ます。関数定義の中に無い return 文は構文エラーになります。そのような JavaScript コードは、値を返すことは出来ません。
ですので、JavaScript からの値を受け取るには、return 文を関数定義の中に入れてやる必要があります。
即時関数は、即座に実行される関数定義です。ですから、Execute(string) メソッドの引数の中に return 文が含まれていても予期した通りに動作します。
即時関数化が邪魔になる場合は、それを行わないバージョンを使用してください。
token
前述のように即時関数化で引数tokenが設定されて、その値として32桁の数値が文字列として与えられます。
CookerのJavaScript拡張は、このtokenを必要とします。例えば、CookerのJavaScriptはローカルファイルへの出力をサポートしていますが、それを使用するには、
1: | // filename.txtへ"output data"を書き出します。 |
2: | window.cooker.storage.Write(token, "filename.txt", "output data", true); |
この様にします。window.cooker.storage.Write()は正しいtokenを与えなければ動作しません。
tokenの値は即時関数化の際に付与されます。これにより、拡張機能の利用をユーザ由来のコードに制限することができます。
Ver 3.0.2でjsexecutorクラスにTokenゲッターが追加されています。このゲッターは上記のtoken値を返します。システムによる変換なしで拡張機能を使用するJavascriptコードを記述したい場合は、このゲッターを使用してください。
即時関数化 - 別の方法
JavaScriptコードの中に命令を直接書き込むことで即時関数化することができます。優先度が最も高い即時関数化命令です。
1: | // 名前付きコメント |
2: | /**JS** |
3: | ///immed |
4: | alert(token); |
5: | */ |
6: | |
7: | /**JSASYNC** |
8: | ///async |
9: | alert(token); |
10: | */ |
11: | |
12: | class main |
13: | rice rr; |
14: | jsexecutor jse; |
15: | open method void start() |
16: | jse.Execute(rr.JS, false); // 即時関数化しないExecute()。しかし、即時関数化される。 |
17: | endmethod |
18: | endclass |
16行目のExecute(rr.JS, false)は即時関数化を行いませんが、JavaScript内の命令が優先されて即時関数化されます。
3行目の///immedと8行目の///asyncが即時関数化命令です。JavaScriptコードの先頭が、これらの文字列ならば即時関数化が行われます。
///immedによる即時関数化は以下の様になります。1行目と4行目が付加されます。
1: | (function(token){ |
2: | ///immed |
3: | alert(token); |
4: | }("0f8fbd59d9cb769fa16570867728950e")); |
///asyncによる即時関数化は以下の様になります。1行目と4行目が付加されます。
1: | (async function(token){ |
2: | ///async |
3: | alert(token); |
4: | }("0f8fbd59d9cb769fa16570867728950e")); |
JavaScript からの返り値
JavaScript からreturn文、throw文を使って値をRiceに返すことができます。
返すことが出来るのは、数値、真偽値、文字列、のみです。さらに、これらが文字列で返ります。
数値が返る場合は、数値へ解析できる文字列が返ります。例えば、1、20000、256.35 等です。
真偽値が返る場合は、文字列で true、false のいずれかが返ります。
文字列が返る場合は、文字列がダブルクオートで囲まれて返ります。例えば、"returned string." の様になります。
オブジェクト、undefined, null などが返った場合は、返り値が上記の型に解析出来ないので、代わりに、文字列 null が返ります。JavaScript の実行がエラーだったり、値を返さない場合も同じです。
上記の規則は、全ての JavaScript からの返り値に適用されます。
script タグの埋め込み
上記の例では、JavaScript コードを直接与えることで実行しましたが、実行するコードをページに埋め込むこともできます。
コードを埋め込むためのメソッドは、以下の3つです。
1: | SetJS(string,string) | script タグを作成し、JavaScript コードを挿入します。 |
2: | SetJSFromFile(string,string) | script タグを作成し、引数のファイルからをJavaScriptコードを読み込んで挿入します。 |
3: | SetSRC(string,string,string) | script タグを作成し、src 属性を設定します。 |
これらのメソッドは、細かい振舞は異なりますが、基本的な動作は全て一緒です。
新しい script タグを作成して、これをドキュメントの body タグの末尾に加えます。
script タグや body タグなどのHTML関連の用語や機能などについては説明を割愛します。
以下は、作成される script タグの例です。
JavaScript コードの埋め込み:
jse.SetJS("function examplefunc(mess){alert(mess);}","exid");
以下の script タグが作成されて body タグの末尾に追加されます。
1: | <script id="exid"> |
2: | function examplefunc(mess){alert(mess);} |
3: | </script> |
src属性:
例として jQuery を読み込んでみます。
jse.SetSRC("https://code.jquery.com/jquery-3.5.1.min.js", "exid", "utf-8");
以下の script タグが作成されて body タグの末尾に追加されます。
1: | <script src="https://code.jquery.com/jquery-3.5.1.min.js" id="exid" charset="utf-8"> |
2: | </script> |
SetSRC() メソッドの実行は非同期に行われます。
外部サイトからの読み込みには時間がかかるため、読み込まれたコードがすぐに存在しない場合があります。
SetJS() メソッドの呼び出しをサンプルコードに追加してみます。
1: | class main |
2: | browser br; |
3: | switch sw; |
4: | message m; |
5: | jsexecutor jse; |
6: | open method void start() |
7: | sw = br.LoadCompleted(this, "LCHandler(dictionary)"); |
8: | sw.Start(); |
9: | br.LoadUrl("https://www.example.com/"); |
10: | endmethod |
11: | |
12: | open method void end() |
13: | sw.Stop(); |
14: | endmethod |
15: | |
16: | open method void LCHandler(dictionary dic) |
17: | if(dic.IsSuccess) |
18: | jse.SetJS("function examplefunc(mess){alert(mess);}","exid"); // 埋め込み。 |
19: | jse.examplefunc("ページロード、コード埋め込み、直接呼び出し成功。"); // 直接呼び出し。 |
20: | else |
21: | jse.Execute("alert('ページロード失敗。');"); |
22: | endif |
23: | endmethod |
24: | endclass |
JavaScript 関数の直接呼び出し
Rice からページ内の JavaScript 関数を呼び出すことが出来ます。
呼び出し方法はいくつかありますが、ここでは JavaScript 関数の直接呼び出し、すなわち、Rice のメソッド呼び出し表記を使用した JavaScript 関数の呼び出し方法について説明します。
上記の example.cook の18、19行目に注目してください。
SetJS() メソッドを使って JavaScript 関数 examplefunc(mess) を埋め込み、直接呼び出しています。
19: | jse.examplefunc("ページロード、コード埋め込み、直接呼び出し成功。"); |
JavaScript 関数を jse から呼び出していますが、呼び出しの外見は、メンバメソッドの呼び出しと変わりません。
jsexecutor クラスはメンバとしての examplefunc(mess) メソッドを持ちませんが、この呼出は成功します。ウェブページに同名の JavaScript 関数が存在するからです。
jsexecutor クラスは存在しないメンバメソッド呼び出しを、JavaScript 関数の呼び出しに変換します。
該当する JavaScript 関数が存在しない場合は、JavaScript で例外が発生して Rice に制御が戻ります。
JavaScript での例外の発生は Rice に通知されません。文字列 null を Rice に返して即座に終了します。
JavaScriptが return null; で終了した場合も Rice は文字列 null を受け取ります。返り値がない場合も文字列 null を受け取ります。
戻り値は同じです。 つまり、これらの状況は区別できません。 この事実は、RiceがJavaScriptで発生する例外を検出できないことを意味します。
JavaScript での例外が重要な場合は、try 文を使い例外を適切な返り値に変換して下さい。
直接呼び出しで JavaScript 関数へ渡せる引数は、int、long、real、bool、string クラスのいずれかに限定されています。
引数の数に制限はありません。JavaScript 関数が必要とする順番で必要な数を渡してください。必要な数より多い引数は無視されます。
examplefunc(mess) 関数は値を返しませんが、JavaScript 関数からの返り値を受け取ることも出来ます。既に説明したように返り値は文字列で戻ります。
jsexecutor クラスのメソッド名と JavaScript 関数名が衝突した場合は、jsexecutor クラスのメソッドの呼び出しが優先されます。
そのような場合に、JavaScript 関数を呼び出すには、CallJSFunction(string,...) を利用してください。
文字列の受け渡し
Rice と JavaScript の間で簡単に大量のデータの移動ができれば便利です。
上述の関数呼び出しを経由すればデータの移動は出来ます。しかし、関数呼び出しはデータを移動するためのものではありませんし、大量のデータ移動には適しません。
Rice と JavaScrip は異なる処理系ですから、複雑なデータを直接移動することは出来ません。
移動できるデータは、int、long、real、bool、string クラスのような基本的なものに制限されています。
この制限下で大量のデータを移動するには、データを文字列にシリアライズして渡すのが現実的でしょう。
Cooker は Rice と JavaScript の間で文字列を受け渡すための機能を備えています。
Rice : jsexecutor クラス
1: | GetData() | JavaScript で設定された文字列を受け取ります。 |
2: | SetData(string) | JavaScript に文字列を渡します。 |
JavaScript : window.cooker.external オブジェクト
1: | GetData(token) | Rice で設定された文字列を受け取ります。非同期関数です。 |
2: | SetData(token,object) | Rice に文字列を送ります。非同期関数です。 |
JavaScript : window.cooker.data オブジェクト
jsexecutor の SetData(string) メソッドの引数は、このオブジェクトにも設定されます。
上記の、jse 変数を利用すると仮定すると、データの授受は次のようになります。
Rice で受け取る。
1: | string dataFromJs = jse.GetData(); |
Rice から送る。
1: | jse.SetData("string data to JavaScript."); |
JavaScript で受け取る。
1: | let dataFromRice = await window.cooker.external.GetData(token); |
もしくは | |
2: | let dataFromRice = window.cooker.data; |
window.cooker.external.GetData(token)は非同期関数です。文字列を受け取るにはawaitする必要があります。awaitは非同期関数の定義内でしか使用できません。///asyncを使って非同期な即時関数化を使用してください。
JavaScript から送る。
1: | window.cooker.external.SetData(token,"string data to Rice."); |
これらを駆使することで Rice と JavaScript の間でデータをやり取りできます。
個々のメソッドや関数の振舞については、上記のリンクを参照してください。
Notice イベント
JavaScript から Rice コードを呼び出せれば便利かもしれませんが、JavaScript から Rice コードを直接に呼び出す方法はありません。
直接に呼び出すことは出来ませんが、Rice に呼び出しを移譲することは出来ます。
JavaScript : window.cooker.external オブジェクト
1: | Notice(token,object) | jsexecutor の Notice イベントを発火します。非同期関数です。 |
例えば、
1: | window.cooker.external.Notice(token,'A string data which will be passed to the Rice.'); |
この関数呼び出しは jsexecutor の Notice イベントを発火します。Notice イベントにハンドラが設定されていれば、Notice 関数の引数が文字列としてイベントハンドラの引数に渡されます。
この引数を判定すれば、適切な Rice コードが実行できます。