普段の仕事で「あ、これはブログに書き留めておこう」と思ったことの半分以上は、仕事の忙しさを理由になかなか書くことが出来なくて、それだけにこうして休日の夜中でもちょっとでも時間に空きがあればブログを書けることがどんなに嬉しいか。
妻の高校の頃のジャージがやけに似合う男、正宗です。
今日は、ActionScript3のExternalInterfaceを使ってJavaScriptと連携する際の、普段から気をつけていることをまとめてみたいと思います。
サンプルのソースはこちら、githubに置いてみました。
AS3でJSとイチャイチャするにはExternalInterfaceを使えばいいというのは周知の事実ですが、いろいろと気をつけておいた方が良いことがたくさんあります。
特に長年やっててまとめようまとめようと思っていた箇所を、いったんここにまとめてみたいと思います。
絶対に気を付ける点はこちらです。
- ObjectタグのIDのどこかに「external」を入れる。
- Javascriptの名前空間を出来るだけ汚染しない。
- 初期化の実行順序を守る
2011/10/23 追記
ひろゆきさんに教えていただいたんですが(こちら)、IDに「external」を付けなくても動くそうです。
AS側でやっといて便利なのは次の点です。
- JavaScriptとの連携は必ずシングルトンを介して行う。
順番にみてみたいと思います。
Cap1.ObjectタグのIDのどこかに「external」を入れる。
こちらの記事にあるように、IEのアホウはトンマなヤロウであるわけです。
本来ならばIEのマヌケなバグにこちらが付き合ってやるいわれはないわけですが、ObjectタグのIDのどこかしらに「external」と入れてやれば万事解決するのであれば、そうしてやろうではありませんか。
それが大人というものではないでしょうか。
なのでSWFObjectの記述はこんな感じになるかと思います。
;(function(){
var swfDirectory = "/";
var swfFileName = "index.swf";
var targetId = "flashcontent";
var width = "800";
var height = "600";
var version = "9.0.45";
var expressInstall = swfDirectory+"commons/scripts/swfobject/expressInstall.swf";
var flashvars = {};
flashvars.jsNamespace = "jp_utweb_ExternalInterface_sample";
var params = {};
params.menu = "false";
params.quality = "best";
params.scale = "noscale";//showAll
params.salign = "tl";
params.wmode = "transparent";//opaque wmode
params.swliveconnect = "true";
params.allowfullscreen = "true";
params.allowscriptaccess = "always";
params.allownetworking = "all";
var attributes = {};
attributes.id = "external_content";
attributes.name = "external_content";
attributes.align = "middle";
swfobject.embedSWF(swfDirectory+swfFileName, targetId, width, height, version, expressInstall, flashvars, params, attributes);
})();
まずは22行目、23行目、ObjectのID属性に「external」が出現していることに注目してください。
10行目の
flashvars.jsNamespace = "jp_utweb_ExternalInterface_sample";
は次のCap2で説明します。
Cap2.Javascriptの名前空間を出来るだけ汚染しない。
基本的には匿名関数とか使って、ぱっと実行された間に必要なときだけ呼び出されてあとは何事もなかったかのようにぱっと消えるのは格好良いんですが、ASからJSを呼び出そうとした場合、いろいろ試してみたんですがどうしても最低ひとつはJSの名前空間を汚染する必要があるという結論に至りました。
これに関しては結構いろいろためして、一見して無駄だと思えるような、匿名関数の中でSWFObjectを実行してその中でSWFはJS側の関数を呼ぶことで全く名前を消費しない方法を使えないかとかいろいろ試したのですが僕のスキルではどうにもならず、結果としてどうしても一個だけ名前を消費する方法しか作れませんでした。
これがそのJavaScriptのテンプレートです。
;var jp_utweb_ExternalInterface_sample;
(function(){
this.getElementFromName = function(name){
if (navigator.appName.indexOf("Microsoft") != -1){
return window[name];
}else{
return document[name];
}
};
jp_utweb_ExternalInterface_sample = function(){};
jp_utweb_ExternalInterface_sample.ready = function(){
//ActionScriptが実行可能状態になったら最初に呼び出される。
//JS側からAS側に関数を呼び出すときは必ずこの関数が実行された後にしなくてはならない。
};
jp_utweb_ExternalInterface_sample.hoge = function(){
//ActionScriptから適時呼び出される。
var swf = this.getElementFromName("external_content");
if(swf){
//ActionScriptの関数を実行。
swf.asFunction();
}
};
})();
一行目の
;var jp_utweb_ExternalInterface_sample;
これが唯一の名前空間となります。
これと全く同じ名前を、Cap1の10行目で
flashvars.jsNamespace = "jp_utweb_ExternalInterface_sample";
というふうにFlashに伝えてあることによって、Flash側ではJavaScriptへの呼び出しを可能にします。
Flash側での呼び出しはCap4で説明します。
コーヒーブレイク:ぼくのかんがえたさいきょうのJS名前空間
一旦休憩です。
非常にくだらない事を書くので、読み飛ばしてCap3まで進めることを強くおすすめします。
僕がJavaScriptを触っていて特に気を遣うのが名前空間の汚染なわけですが、大規模案件にかかわらず一人で開発しているような趣味のプログラミングでもどうしてもグローバルの名前空間の汚染が避けられない状況がよく出てきます。
僕は僕なりにいろいろ調べてはみたんですが、JavaScriptは言語仕様としてどうしてもこれは避けられないのではないかと思うようになったわけです。
名前空間を擬似的に扱うクラスを作ってみたりもしたんですが、そのクラスに一つ名前空間を使うという仕様になってしまい、これはだめだということで他のアプローチで名前空間を確保する方法を考えてみました。
1:僕を褒める言葉を使用する。
調べてみたんですが、少なくともインターネット上では僕を褒める言葉が使われている回数は皆無なのですね。
そこでこれを利用して僕を褒め称える変数名を使用すると、絶対に名前空間が保証されるというアイデアです。
こんなふうに使います。
var 正宗さんは格好いい = "ほげ"; var 正宗さんの言うことは常に正しい = "ふが";
2行目のように、僕を恒常的に肯定する言葉を用いるとより強固になります。
2:自作ポエムを変数名にする。
変数名などを自作のポエムにすることで著作権によってプログラマーの死語50年間まで担保される非常に強固な名前空間を仕様することができます。
この場合の名前空間の強度はディズニー法の強度に依存します。
var かにをたべ るとくちのなか かゆくなる = "hoge";
プログラムが一気に風流になるという副次的効果付きでとてもおすすめです。
仕方のない話しはここまでにして、本題に戻ります。
今まではSWFを表示させるためにブラウザ側やHTML側、JavaScript側の問題でしたが、次はActionScript側の問題になります。
Cap3.初期化の実行順序を守る
よっぽどの理由がない限り、JS側からAS側に働きかける場合、AS側が準備完了かどうかを待ってJSは働きかけたほうがより安全だと思います。
つまりAS側が準備完了になった時点ではじめてJSに通知して、JSはその通知を待ってASに働きかけるようにするのが定石と言えます。
Cap2の11行目から14行目
jp_utweb_ExternalInterface_sample.ready = function(){
//ActionScriptが実行可能状態になったら最初に呼び出される。
//JS側からAS側に関数を呼び出すときは必ずこの関数が実行された後にしなくてはならない。
};
を用意しておき、必ずActionScript側から最初に呼び出されてJSの初期化をここで行うようにします。
ここまでが僕が作った必ず守るべきルールになります。
次からはActionScript側からJSと連携するのに便利な方法をまとめたものになります。
Cap4.JavaScriptとの連携は必ずシングルトンを介して行う。
この記事の最初に提示したgithubのサンプルソースを見ていただくとわかりやすいんじゃないかと思うんですが、Javascriptとの連携はそれを専門にしたシングルトンクラスにまかせることにします。
ちょっと簡単に書いてみます。
ActionScript3です。
package
{
import flash.events.Event;
import flash.events.EventDispatcher;
import flash.external.ExternalInterface;
public class JavascriptTool extends EventDispatcher
{
public static const DO_HOGEHOGE:String = "doHogehoge";
public static const DO_FUGAFUGA:String = "doFugafuga";
private static var _instance:JavascriptTool;
private var _jsNamespace:String;
public static function getInstance(jsNamespace:String = null):JavascriptTool
{
if(!JavascriptTool._instance){
JavascriptTool._instance = new JavascriptTool(new Enforcer());
if(!jsNamespace){
throw new JsNamespaceError("Javascriptと連携する名前空間を指定してください。");
}else{
JavascriptTool._instance._jsNamespace = jsNamespace;
}
try{
ExternalInterface.addCallback("jsHoge", JavascriptTool._instance.hogehoge);
ExternalInterface.addCallback("jsFuga", JavascriptTool._instance.fugafuga);
}catch(error:Error){
}
}
return JavascriptTool._instance;
}
public function ready():void
{
try{
ExternalInterface.call(_jsNamespace + ".ready");
}catch (error:Error){
}
}
public function hogehoge():void
{
dispatchEvent(new Event(DO_HOGEHOGE));
}
public function fugafuga():void
{
dispatchEvent(new Event(DO_FUGAFUGA));
}
function JavascriptTool(enforcer:Enforcer):void{}
}
}
class Enforcer{}
class JsNamespaceError extends Error{
public function JsNamespaceError(message:String):void
{
super(message);
}
}
シングルトンの説明は省いたとして、これ。
ExternalInterface.addCallback
第一引数がJavascript側から見える関数。
第二引数が実際のActionScriptの関数。
APIとして多言語に対しての接続部分が隠匿化できるので非常に優れてるといえますけど作業する本人達にとってはややこしいことこの上ないです。
この部分を埋めるデバッグツールは存在しないわけですし。
そうは言ってもわざわざJS側とAS側で呼び出す関数名を指定できるってことはJS側とAS側で違う名前にしておいたほうがいいって事ですかね?
僕はそう解釈して実行していますが、このへんの名前の命名規則になにか良い方法があったら教えていただきたいです。
あと21行目、他はイレギュラーな使われ方をしたときにわざわざコンパイルエラーを出すように作ってあるのに対して、この部分だけはランタイムエラーになるようにしか作れませんでした。
シングルトンの生成される最初だけ値を渡して初期化したいときに、初期化のタイミングで値が渡されたときとそれ以外で値が渡されたときの両方でコンパイルエラーを出す何か上手い方法があればどなたか教えてください。
というわけでまだまだ未完成ながらもこのシングルトンクラスをいきなりドキュメントクラスで使ってみたのがこちら。
ActionScript3です。
package
{
import flash.display.Sprite;
import flash.events.Event;
import flash.text.TextField;
import flash.text.TextFieldAutoSize;
public class externalinterfaceSample extends Sprite
{
private static const _DEFAULT_JS_NAMESPACE:String = "hogehoge_msturi";
public function externalinterfaceSample()
{
if(stage){
_init();
}else{
addEventListener(Event.ADDED_TO_STAGE, _init);
}
}
private function _init(e:Event = null):void
{
if(!(!e)){
removeEventListener(Event.ADDED_TO_STAGE, _init);
}
var tf:TextField = new TextField();
tf.border = true;
tf.autoSize = TextFieldAutoSize.LEFT;
addChild(tf);
var jsNamespace:String = _DEFAULT_JS_NAMESPACE;
var flashVars:Object = stage.loaderInfo.parameters;
if(!(!flashVars.jsNamespace) && flashVars.jsNamespace.length > 0){
jsNamespace = flashVars.jsNamespace;
}
var jsTool:JavascriptTool = JavascriptTool.getInstance(jsNamespace);
jsTool.ready();
tf.text = "Javascriptからの呼び出しが実行可能になった。";
jsTool.addEventListener(JavascriptTool.DO_HOGEHOGE, function(e:Event):void{
tf.text = "JavascriptからjsHogeが呼び出された。";
});
jsTool.addEventListener(JavascriptTool.DO_FUGAFUGA, function(e:Event):void{
tf.text = "JavascriptからjsFugaが呼び出された。";
});
}
}
}
こんな感じでどこでもシングルトンクラスからJSを実行できるし、逆にJSからの呼び出しもいつでもどこでもEventを受け取る形でキャッチできるようになるんだよ〜、というサンプルでした。
ActionScript、スマートすぎる。
このCap4は全てgithubのままですが、最後にいちおうgithubのindex.htmlのソースも載せておきます!
ExternalInterfaceの実験
ちょっと実験用に名前のスコープをいじっております。
特にjp_utweb_ExternalInterface_sampleを返す48行目は、明示的にpublicな関数を指定してやるとJavaScript側からも実行できるようになります。
まとめ
やっと最近コードスニペットというのを覚えたので、コードの再生産がだいぶマシになりました。
毎回毎回こんだけ再発明するのしんどいですもんね。
まだまだ未完成な部分も多いですが、もっと上手い方法を随時見つけていきたいと思います!