JavaScriptから外部JavaScriptを読み込む方法

最新版はこちら → JavaScriptで外部ライブラリを読み込むためのスクリプトをCodeRepos.orgに上げた。 - ヒルズで働く@robarioの技ログ


改良に伴い、エントリーを全面的に書き換えました。
  • 後から再利用できるように名前を付けました(ScriptRunner)
  • 外部JavaScriptを読み込むタイミングを最初に持ってきました。今まで外部JavaScriptを読み込むだけの場合でもScriptRunner({...}) ();と書かないといけなかったのですがScriptRunner({...});と書けるようになりました。
  • 外部JavaScriptの読み込み後、arguments.calleeを返すようにしました。これにより、ScriptRunner({...}) () () () () () () ;と書いても動くようになりました。
  • 外部JavaScriptの読み込み完了を待つかどうか指定できるようになりました。
  • IEの場合、script要素を追加してもコンテキストがwindowじゃない時(何て言えばいいのかわからん。要するにthis!=windowの時)、読み込み自体は行うのですが、評価はしてくれないようなのでsetTimeoutで逃げることにしてみました。
  • 「w=window;t=setTimeout」となっていてtをグローバルに突っ込んでいたのを「w=window,t=setTimeout」に修正。nをグローバルに突っ込んでいたのを修正。

結構使い勝手が良くなってきました。
元のスクリプトを一切変更せず直前にコードを挿入するだけで良いのがポイントです。
HTML側に手を入れる必要がないため、外部JavaScriptを読み込むタイプのブックマークレットでは、読み込まれたJavaScriptの内部から更に他の外部JavaScriptを読み込むことができます。


Sjaxを使わないJavaScript Loader - ヒルズで働く@robarioの技ログから派生しています。

JavaScriptを書いてみる
→外部JavaScriptを読み込みたいなー。
→document.write('<script...')したら画面が真っ白Σヽ(゚Д゚; )ノ
→document.appendChild(script)でOK!+。:.゚ヽ(*´∀`)ノ゚.:。+゚
→外部JavaScriptの読み込みが終わってないのに処理が進んじゃうー(><)
→えーい、3秒間待ってやるo(`ω´*)o
→setTimeout(...,3000) 何か格好悪い(´・ω・`)
そんな方に捧げるエントリー。

外部JavaScriptの読み込み方

JavaScript内から外部JavaScriptを読み込む場合、<script>タグをdocument.writeする方法・script要素をDOMで追加する方法・Ajaxで読み込む方法があります。それぞれメリット・デメリットがあります。

<script>タグをdocument.writeする
  • documentがcloseされていないことが必要

window.onload内やBookmarkletでは既にdocumentがcloseされてしまっているのでこの方法は使えません。

  • 読み込みが完了するまで停止する

外部JavaScriptの読み込みが完了してから次の行へ行くため、後続するJavaScriptは読み込みが完了していることを前提に書くことができます。
ただしブラウザの処理が停止してしまうため、外部JavaScriptの読み込みに時間がかかるとページレンダリングがそこで止まってしまいます。

script要素をDOMで追加する
  • いつでも使える
  • 読み込み中もブラウザの処理は止まらない

ただし読み込みがいつ完了したか分かりません。

Ajaxで読み込む

痛いです。

  • Sjaxで読み込んでいる間、ブラウザの処理が停止する

<script>タグをdocument.writeする場合と同じです。

今回の話

script要素をDOMで追加し、windowオブジェクトの変化を調べて読み込まれたかどうかを判断する方法もあるよ、というのが前回の話。
今回は「既存のJavaScriptに簡単に付けたり外したりできるようにしましょう」という話です。

条件

まず

  • JavaScriptが (function(){/*...*/})() という形になっている
  • 外部スクリプトがwindowオブジェクトにプロパティを追加する

という条件を満たす必要があります。1番目は構文的な制限で、2番目は仕組み的な制限です。

1番目の条件を満たさない場合、大抵は「(function(){」と「})()」で囲めば上手くいきます。

追加するコード

既存のJavaScriptの直前にこんなコードを挿入します。

(ScriptRunner = function(libs) {
    var waiting = {};
    for (var i = 0, n = libs.length; i < n; ++i) {
        if (typeof libs[i] == 'string' || libs[i] instanceof String) {
            libs[i] = {'': libs[i]};
        }
        for (var prop in libs[i]) {
            if (prop) {
                if (window[prop]) {
                    continue;
                }
                waiting[prop] = 1;
            }
            (function(src){
                var script = document.createElement('script');
                script.type = 'text/javascript';
                script.charset = 'UTF-8';
                script.src = src;
                setTimeout(function(){document.documentElement.appendChild(script)}, 0);
            })(libs[i][prop]);
        }
    }
    return function(func) {
        if (func) {
            setTimeout(function() {
                for (var prop in waiting){
                    if (!window[prop]) {
                        return setTimeout(arguments.callee, 99);
                    }
                }
                func();
            }, 0);
        }
        return arguments.callee;
    }
})
([
    // ここで読み込みたいライブラリを指定
])
// ↓ここから既存のJavaScript

短くまとめるとこうなります。どちらを使っても構いません。

(ScriptRunner=function(v){var i,n,l,p,c={},w=window,t=setTimeout;for(i=0,n=v.length;i<n;++i){l=v[i];if(typeof l=='string'||l instanceof String){l={'':l}}for(p in l){if(p){if(w[p]){continue}c[p]=1}(function(j){var s=document.createElement('script');s.type='text/javascript';s.charset='UTF-8';s.src=j;t(function(){document.documentElement.appendChild(s)},0)})(l[p])}}return function(f){if(f){t(function(){for(p in c){if(!w[p]){return t(arguments.callee,99)}}f()},0)}return arguments.callee}})
([
    // ここで読み込みたいライブラリを指定
])
// ↓ここから既存のJavaScript

functionを2段階に分けて返しているのには意味があります。
今回のようなことをするには、既存のJavaScriptを関数オブジェクトとして受け取る必要があります。
で、「単純に『受け取った関数』を実行する関数」を使うと

function foo(f) {
    f();
}
foo(function(){        // ↑ここまで追加
(function(){...})()
});                     // ここにも修正が必要!

のように全体を囲む必要があります。末尾の修正が嫌な感じです。
それを無くすには太字の(,)を「関数の引数を示す括弧」とみなして、「【『受け取った関数』を実行する関数】を返す関数」を前置すれば良いのです。

function foo(f) {
    return function() {
        f();
    }
}

foo                    // ↑ここまで追加  ↓ここから修正不要
(function(){...})()

このようにしておけば、追加コード部分をばっさり外部JavaScriptの内容に置き換える(静的に展開する)のも楽です。
というか、元々の目的はそこだったりします。開発中は外部JavaScriptを動的に読み込み、リリース時は一つのJavaScriptファイルにまとめる、といったことがPerlで簡単に書けます。

例えばこんなスクリプトがあるとき

(function(){
    alert(typeof JSONScriptRequest);
})();

このまま実行すると当然ですが"undefined"が出力されます。そこで直前に上記コードを挿入します。

(ScriptRunner=function(v){var i,n,l,p,s,c={},w=window,t=setTimeout;for(i=0,n=v.length;i<n;++i){l=v[i];if(typeof l=='string'||l instanceof String){l={'':l}}for(p in l){if(p){if(w[p]){continue}c[p]=1}s=document.createElement('script');s.type='text/javascript';s.charset='UTF-8';s.src=l[p];document.documentElement.appendChild(s)}}return function(f){if(f){t(function(){for(p in c){if(!w[p]){return t(arguments.callee,99)}}f()},0)}return arguments.callee}})
([
    {JSONScriptRequest: 'http://www.openjsan.org/src/y/yo/yoshida/JSONScriptRequest-0.02/lib/JSONScriptRequest.js'}
])
(function(){
    alert(typeof JSONScriptRequest);
})();

これを実行すると、JSONScriptRequest.jsの読み込みが完了してからalertが実行されるので、必ず"function"が出力されます。

外部JavaScriptの読み込み完了を待つ必要がない場合、objectの変わりにstringを渡します。

(ScriptRunner=function(v){var i,n,l,p,s,c={},w=window,t=setTimeout;for(i=0,n=v.length;i<n;++i){l=v[i];if(typeof l=='string'||l instanceof String){l={'':l}}for(p in l){if(p){if(w[p]){continue}c[p]=1}s=document.createElement('script');s.type='text/javascript';s.charset='UTF-8';s.src=l[p];document.documentElement.appendChild(s)}}return function(f){if(f){t(function(){for(p in c){if(!w[p]){return t(arguments.callee,99)}}f()},0)}return arguments.callee}})
([
    'http://www.openjsan.org/src/y/yo/yoshida/JSONScriptRequest-0.02/lib/JSONScriptRequest.js'
])
(function(){
    alert(typeof JSONScriptRequest);
})();

ただしこの例では"undefined"が出力されます。JSONScriptRequest.jsの読み込みが完了する前にalertが実行されてしまうからです。