LoginSignup
3
2

More than 5 years have passed since last update.

ServiceWorkerを使ってブラウザでCommonJS風のrequireが使えるようにしてみたい。

Last updated at Posted at 2016-12-31

ServiceWorker使えばrequirejsでcommonjs風のrequireを実現している方法を参考に、なんとかできるのではないかと思って、
突貫工事でちょっとだけ動きそうなものを作ってみました。

参考

ググったら出てきた。
https://github.com/spite/service-worker-require/blob/master/service-worker.js
ただ、この人のイメージはServiceWorkerでバンドルしているイメージなので、もう少し頑張れないか考えてみました。

コード

commonjs-wrapper.js
/*global Promise*/
(function() {
    'use strict';
    navigator.serviceWorker.register('commonjs-wrapper-service-worker.js').then(function(reg) {
        reg.addEventListener('updatefound', function() {
            //アップデートされたので再読み込み
            location.reload(true);
        });

        if (!navigator.serviceWorker.controller) {
            //初回登録されたので再読み込み
            location.reload(true);
        }
    });

    function Module() {
        this.exports = {};
    }

    var listeners = {};

    var modulesMap = {};

    var scripts = document.getElementsByTagName('script');
    var getScript = function(url) {
        for (var i = 0; i < scripts.length; i++) {
            if (scripts[i].src === url) {
                return scripts[i];
            }
        }
        return null;
    };
    var getBeforeScripts = function(url) {
        var beforeScripts = [];
        for (var i = 0; i < scripts.length; i++) {
            if (scripts[i].src === url) {
                return beforeScripts;
            }
            beforeScripts.push(scripts[i]);
        }
        return beforeScripts;
    };
    var hasScript = function(url) {
        return !!getScript(url);
    };

    //script本体実行
    var executeScript = function(url, dependenciesMap, fn) {
        var module = new Module();
        var require = function(id) {
            var depUrl = dependenciesMap[id];
            return modulesMap[depUrl].exports;
        };

        fn(module.exports, require, module);

        modulesMap[url] = module;

        if (listeners[url]) {
            listeners[url].forEach(function(listener) {
                listener();
            });
            delete listeners[url];
        }
    };

    //commonjsラッパーのラッパーメソッド
    window.commonjswrapper = function(url, dependenciesMap, fn) {
        var dependencyUrls = Object.keys(dependenciesMap).map(function(k) {
            return dependenciesMap[k];
        });
        var selfScript = getScript(url);

        if (!selfScript.hasAttribute('data-commonjs-wrapper-dependency')) {
            //commonjswrapperの追加分でない場合、自身よりも前のscriptもdependency対象とする
            dependencyUrls = dependencyUrls.concat(getBeforeScripts(url).filter(function(script) {
                return script.hasAttribute('data-commonjs-wrapper-dependency');
            }).map(function(script) {
                return script.src;
            }));
        }
        //必要なModule
        var needUrils = dependencyUrls.filter(function(depUrl) {
            return !modulesMap[depUrl];
        });
        //追加で必要なModuleが無ければscript実行
        if (!needUrils.length) {
            executeScript(url, dependenciesMap, fn);
            return;
        }

        //必要なModuleを追加
        Promise.all(needUrils.map(function(url) {
            if (!hasScript(url)) {
                var script = document.createElement('script');
                script.src = url;
                script.setAttribute('data-commonjs-wrapper-dependency', true);
                document.head.appendChild(script);
            }

            return new Promise(function(resolve) {
                var listenerList = listeners[url] || (listeners[url] = []);
                listenerList.push(function() {
                    resolve();
                });
            });
        })).then(function() {
            //Moduleがすべて読み込まれたら、自身のscriptを実行
            executeScript(url, dependenciesMap, fn);
        });
    };

    //自身が登録済であることを指示
    if (window.___url___) {
        modulesMap[window.___url___] = new Module();
    }
})();
commonjs-wrapper-service-worker.js
/*global Promise*/
(function() {
    'use strict';

    //モジュールのURL取得
    var getModleUrl = function(sourceUrl, module) {
        //TODO npmのurlで取れるようにしたい。
        //相対から絶対PATHへ変換
        return new Request(sourceUrl + '/../modules/' + module + '/index.js').url;
    };

    //ラップしたスクリプトを返す
    var createCommonJsWrapper = function(url, dependencies, originalScript) {
        var res = new Response('(window.commonjswrapper || function(url, dependenciesMap, fn) {window.___url___ = url; fn(); delete window.___url___;} )("' +
            url +
            '", {' +
            dependencies.map(function(s) {
                return '"' + s + '":"' + getModleUrl(url, s) + '"';
            }).join(',') +
            '}, function (exports, require, module) {' +
            originalScript +
            '});'
        );
        res.headers.append('Content-Type', 'text/plain;charset=UTF-8');
        return res;
    };

    this.addEventListener('fetch', function(event) {
        var url = event.request.url;

        if (url.indexOf('.js') !== -1) {
            event.respondWith(new Promise(function(resolve) {
                fetch(event.request).then(function(res) {
                    if (!res.ok) {
                        return resolve(res);
                    }
                    return res.text();
                }).then(function(textScript) {
                    //"require"を呼び出している箇所を探す。
                    //参考にしました。 https://github.com/spite/service-worker-require
                    var dependencies = [];
                    var re = /require[\s]*\([\s]*'([\S]+)'[\s]*\)/gmi;
                    var m;
                    while ((m = re.exec(textScript)) !== null) {

                        if (m.index === re.lastIndex) {
                            re.lastIndex++;
                        }
                        //必要なモジュールを追加
                        dependencies.push(m[1]);
                    }

                    resolve(createCommonJsWrapper(url, dependencies, textScript));
                });
            }));

        } else {
            event.respondWith(fetch(event.request));
        }

    });
}).bind(self)();

使い方

commonjs-wrapper.jsとcommonjs-wrapper-service-worker.jsをhtmlと同じディレクトリに置いて、
htmlで、commonjs-wrapper.jsスクリプトを読み込む。

その他のscriptは、

main.js
var hoge = require('hoge');

みたいな記述で、./modules/hoge/index.js モジュールが読み込めるようになる。
(突貫なので、./modules/***/index.js は固定・・・)

説明

commonjs-wrapper-service-worker.js

記載した順番とは逆ですけど、先にcommonjs-wrapper-service-worker.jsの説明をします。

読み込んだ、jsファイルの中身の最初と最後に必要なscriptを記述(ラップ)して返します。
すこし編集してしまいますが行は変わらないので、デバッグでは便利になっていると思いたいです。

commonjs-wrapper.js

ラップメソッド本体を持っています。

service-workerを登録したりしていますがそこは重要じゃないので端折ります。

    //commonjsラッパーのラッパーメソッド
    window.commonjswrapper = function(url, dependenciesMap, fn) {

とりあえず、引数の説明から。

  • url
    scriptのurlです。
  • dependenciesMap
    scriptでrequireが記述された中身と、urlのセットが入ります。
  • fn
    script本体のcommonjsラッパーメソッドです。

fnを実行する前に、dependenciesMapに入った、モジュールのscriptが読み込まれるのを待ち、
最終的にfnを実行します。このあたりはrequirejsの仕組みと同じですね。

もう少し頑張ってることとしては、htmlに記述されたscriptの実行順序を保証するために、
自身のscriptより前に記述されたscriptが読み込まれるのを待ちます。

あとは適当に待って実行してるだけです。

本当は、package.json見たり、npmのunpkgから取ってきたりとかできてくるといい感じになるかもしれないと思いつつ。
時間とれないのでとりあえず諦めて投稿します。

まとめ

ふつうにwebpackとか使いますね。

デバッグを便利にしたかったのだけど、もっと真面目に作らないと全然使えませんね。

とりあえず2016年中に40記事達成:raised_hands:

3
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
2