コンパイル/サーバーなしでブラウザにCommonJSモジュールを実装する

コンパイル/サーバーなしでブラウザにCommonJSモジュールを実装する

導入

私はよく Github にアクセスします。Github では、星の数が非常に多いいくつかの大規模プロジェクトに加えて、興味深い小規模プロジェクトも数多く見つかります。プロジェクトやアイデアは非常に興味深く、技術的な点でも優れているため、読む価値はあります。そこで、Github で見つけた興味深いプロジェクトを時折共有したり解説したりしながら、「Roaming Github」シリーズにまとめるつもりです。このシリーズでは、ソースコードの詳細に立ち入ることなく、原則を説明することに重点を置いています。

さて、本題に入りましょう。今回紹介するリポジトリは one-click.js といいます。

1. one-click.jsとは

one-click.js は非常に興味深いライブラリです。 Github では次のように紹介しています:

Commonjs のモジュール コードをブラウザーで正常に実行したい場合は、通常、webpack、rollup などのビルド/パッケージ化ツールが必要であることがわかっています。 One-click.js を使用すると、これらのビルド ツールを必要とせずに、ブラウザーで CommonJS ベースのモジュール システムを通常どおり実行できます。

さらに、サーバーを起動する必要もありません。たとえば、one-click.js プロジェクトのクローンを作成し、example/index.html ファイルをダブルクリック (ブラウザーで開く) して実行することができます。

リポジトリには、その機能を要約した文章があります。

ビルド手順や Web サーバーを使用せずに、ブラウザーで直接 CommonJS モジュールを使用します。

例えば、

現在、現在のディレクトリ (demo/) に 3 つの「モジュール」ファイルがあるとします。

デモ/plus.js:

// プラス.js
module.exports = 関数 plus(a, b) {
    a + b を返します。
}

デモ/divide.js:

//divide.js
module.exports = 関数divide(a, b) {
    a / b を返します。
}

エントリ モジュール ファイル demo/main.js:

// メイン.js
const plus = require('./plus.js');
定数divide = require('./divide.js');
console.log((12を割り、(1、2)を加算));
// 出力: 4

一般的な使用法は、エントリを指定し、それを webpack でバンドルにコンパイルし、ブラウザーで参照することです。 One-click.js を使用すると、これらすべてを破棄して HTML 内でのみ使用できます。

<!DOCTYPE html>
<html lang="ja">
<ヘッド>
    <メタ文字セット="UTF-8">
    <title>ワンクリックの例</title>
</head>
<本文>
    <script type="text/JavaScript" src="./one-click.js" data-main="./main.js"></script>
</本文>
</html>

スクリプト タグの使用に注意してください。data-main はエントリ ファイルを指定します。このとき、このローカル HTML ファイルをブラウザで直接開くと、結果 7 が正常に出力されます。

2. パッケージングツールはどのように機能しますか?

前のセクションでは、one-click.js の機能を紹介しました。その核となるのは、パッケージ化やビルドなしでフロントエンドのモジュール化を実現することです。

内部実装を紹介する前に、まずパッケージング ツールが何をするかを理解しましょう。諺にあるように、自分と敵を知れば、百戦危うくない戦いができる。

もう一度、3 つの JavaScript ファイルを紹介します。

plus.js:

// プラス.js
module.exports = 関数 plus(a, b) {
    a + b を返します。
}

分割.js:

//divide.js
module.exports = 関数divide(a, b) {
    a / b を返します。
}

エントリ モジュール main.js:

// メイン.js
const plus = require('./plus.js');
定数divide = require('./divide.js');
console.log((12を割り、(1、2)を加算));
// 出力: 4

webpack を使用する場合は、エントリ ポイント (main.js) を指定することを思い出してください。 webpack はエントリに基づいてバンドル (たとえば、bundle.js) をパッケージ化します。最後に、処理された bundle.js をページにインポートできます。現時点では、bundle.js にはソースコードに加えて、webpack の「プライベートグッズ」が多数追加されています。

webpack に含まれる作業を簡単に整理してみましょう。

  • 依存関係分析: まず、パッケージ化時に、webpack は構文分析の結果に基づいてモジュールの依存関係を取得します。簡単に言うと、CommonJS では、解析された require 構文に基づいて、現在のモジュールが依存するサブモジュールが取得されます。
  • スコープの分離と変数の挿入: 各モジュール ファイルに対して、webpack はそれを関数内にラップします。これにより、module や require などの変数を挿入できるだけでなく、スコープを分離して変数のグローバルな汚染を防ぐこともできます。
  • モジュール ランタイムを提供する: 最後に、require と exports を効果的に実行するには、モジュールの読み込み、実行、エクスポートなどの機能を実装するためのランタイム コードのセットも必要です。

上記2、3の項目が分からない場合は、こちらの記事からwebpackのモジュールランタイム設計について学ぶことができます。

3. 直面する課題

ビルド ツールがない場合、CommonJS を使用してモジュールをブラウザーで直接実行するには、上記の 3 つのタスクを実行する方法を見つける必要があります。

  • 依存関係分析
  • スコープ分離と変数の挿入
  • モジュールランタイムの提供

これら 3 つの問題を解決することが、one-click.js の中心的なタスクです。一つずつ解決方法を見ていきましょう。

3.1. 依存関係の分析

これは厄介な問題です。モジュールを正しくロードするには、モジュール間の依存関係を正確に把握する必要があります。たとえば、上記の 3 つのモジュール ファイル (main.js) は plus.js と division.js に依存しているため、main.js のコードを実行するときは、plus.js と division.js がブラウザー環境に読み込まれていることを確認する必要があります。しかし、問題は、コンパイル ツールがなければ、当然ながらモジュール間の依存関係を自動的に知ることができないことです。

RequireJS のようなモジュール ライブラリの場合、コード内で現在のモジュールの依存関係を宣言し、非同期読み込みとコールバックを使用します。どうやら、CommonJS 仕様にはそのような非同期 API はないようです。

One-click.js は、モジュール ファイルを 2 回読み込むという、複雑だがコストのかかる方法で依存関係を分析します。モジュール ファイルが初めてロードされるときに、モジュール ファイルにモック require メソッドが提供されます。モジュールがこのメソッドを呼び出すたびに、現在のモジュールが require 内のどのサブモジュールに依存しているかを知ることができます。

// メイン.js
const plus = require('./plus.js');
定数divide = require('./divide.js');
console.log(マイナス(12、加算(1、2)));

たとえば、上記の main.js では、次のような require メソッドを提供できます。

const 記録フィールドアクセスByRequireCall = {};
const require = 関数 collect(modPath) {
    記録されたフィールドアクセスにより呼び出しが必要[modPath] = true;
    var スクリプト = document.createElement('script');
    スクリプトをロードします。
    document.body.appendChild(スクリプト);
};

main.js が読み込まれた後、次の 2 つの処理が行われます。

  • 現在のモジュールが依存するサブモジュールを記録します。
  • サブモジュールをロードします。

このようにして、recordedFieldAccessesByRequireCall に現在のモジュールの依存関係を記録し、同時にサブモジュールをロードすることができます。サブモジュールは、新しい依存関係が現れなくなるまで再帰的に操作することもできます。最後に、各モジュールのrecordedFieldAccessesByRequireCallを統合することが依存関係です。

さらに、サブモジュール内のどのメソッドが実際に main.js によって呼び出されるかを知りたい場合は、Proxy を使用してプロキシ オブジェクトを返し、さらに依存関係をカウントすることができます。

const require = 関数 collect(modPath) {
    記録されたフィールドアクセスByRequireCall[modPath] = [];
    var megaProxy = 新しいProxy(function(){}, {
        取得: 関数(ターゲット、プロパティ、レシーバー) {
            if(prop == Symbol.toPrimitive) {
                関数() {0;} を返します。
            }
            megaProxy を返します。
        }
    });
    var recordFieldAccess = 新しいProxy(function(){}, {
        取得: 関数(ターゲット、プロパティ、レシーバー) {
            window.recordedFieldAccessesByRequireCall[modPath].push(prop);
            megaProxy を返します。
        }
    });
    // ...その他の処理 return recordFieldAccess;
};

上記のコードは、インポートされたモジュールの属性を取得するときに使用される属性を記録します。

上記のすべてのモジュールのロードは、依存関係を分析するために使用される「2 回のロード」と呼ばれる最初のパスです。 2 回目は、エントリ モジュールの依存関係に基づいてモジュールを「逆」にロードするだけです。たとえば、main.js が plus.js と division.js に依存している場合、実際の読み込み順序は plus.js->divide.js->main.js になります。

すべてのモジュールの最初の読み込み時に、これらのモジュールは基本的に実行時にエラーを報告することに注意してください (依存関係の読み込み順序が間違っているため)。実行エラーは無視し、依存関係の分析のみに焦点を当てます。依存関係を取得したら、すべてのモジュール ファイルを正しい順序で再ロードします。 one-click.js にはより完全な実装があります。このメソッドは scrapeModuleIdempotent と呼ばれます。具体的なソース コードはここにあります。

この時点で、「すべてのファイルを 2 回読み込むのは無駄だ」と考えるかもしれません。

実際、これは one-click.js のトレードオフでもあります。

これをオフラインで動作させるには、One Click はモジュールを 2 回初期化する必要があります。1 回はページの読み込み時にバックグラウンドで依存関係グラフをマッピングし、もう 1 回は実際にモジュールの読み込みを実行します。

3.2. スコープの分離

モジュールには非常に重要な機能があることはわかっています。モジュール間のスコープは分離されています。たとえば、次の通常の JavaScript スクリプトの場合:

// 通常のscript.js
var foo = 123;

ブラウザに読み込まれると、foo 変数は実際にはグローバル変数になり、window.foo を通じてアクセスできるようになります。これによりグローバル汚染も発生し、モジュール間の変数とメソッドが競合して上書きされる可能性があります。

NodeJS 環境では、CommonJS 仕様を使用しているため、上記のようなモジュール ファイルをインポートすると、foo 変数のスコープはソース モジュール内だけになり、グローバル スコープを汚染することはありません。実装の点では、NodeJS は実際にモジュール内のコードを wrap 関数でラップします。ご存知のとおり、関数は独自のスコープを形成し、分離を実現します。

NodeJS は要求時にソース コード ファイルをパッケージ化し、webpack などのパッケージ化ツールはコンパイル時にソース コード ファイルを書き換えます (同様のパッケージ化)。 one-click.js にはコンパイルツールがないので、コンパイル時に書き直すのは絶対に無理です。ではどうすればいいのでしょうか?一般的な方法は 2 つあります。

3.2.1. JavaScript での動的コード実行

1 つの方法は、フェッチ要求を通じてスクリプト内のテキスト コンテンツを取得し、新しい関数または eval を通じて動的なコード実行を実装することです。 fetch+new 関数の使用例を次に示します。

上記の除算モジュールdivide.jsを少し変更して使用すると、ソースコードは次のようになります。

// スクリプトとして読み込まれると、この変数は window.outerVar のグローバル変数になり、汚染を引き起こします。var outerVar = 123;

モジュール.エクスポート = 関数 (a, b) {
    a / b を返します。
}

次に、スコープ シールドを実装します。

modMap は定数です。
関数 require(modPath) {
    modMap[modPath]の場合{
        modMap[modPath].exportsを返します。
    }
}

フェッチ('./divide.js')
    .then(res => res.text())
    .then(ソース => {
        const mod = 新しい Function('exports', 'require', 'module', source);
        定数modObj = {
            id: 1,
            ファイル名: './divide.js',
            親: null、
            子供たち: []、
            エクスポート: {}
        };

        mod は、 modObj をエクスポートして、 .mod をリロードします。
        modMap['./divide.js'] = modObj;
        戻る;
    })
    .then(() => {
        定数divide = require('./divide.js')
        コンソール.log(divide(10, 2)); // 5
        console.log(window.outerVar); // 未定義
    });

コードは非常にシンプルです。核となるのは、fetch を通じてソース コードを取得し、new Function を通じて関数内にソース コードを構築し、呼び出し時にいくつかのモジュール ランタイム変数を「挿入」することです。コードのスムーズな実行を保証するために、モジュール参照を実装するための単純な require メソッドも提供されています。

もちろん、上記は解決策ではありますが、one-click.js の目的には適っていません。 one-click.js もサーバーなし (オフライン) で実行することを目的としているため、フェッチ リクエストは無効です。

では、one-click.js はこれをどのように処理するのでしょうか?次の点を見てみましょう。

3.2.2. スコープを分離する別の方法

一般的に言えば、分離の必要性はサンドボックスの必要性と非常に似ており、フロントエンドにサンドボックスを作成する一般的な方法は iframe を使用することです。便宜上、ユーザーが実際に使用するウィンドウを「メインウィンドウ」、その中に埋め込まれた iframe を「サブウィンドウ」と呼びます。 iframe の本来の特性により、各子ウィンドウには独自のウィンドウ オブジェクトがあり、互いに分離されているため、メイン ウィンドウや他のウィンドウを汚染することはありません。

以下では、divide.js モジュールの読み込みを例として説明します。まず、スクリプトを読み込むための iframe を構築します。

var iframe = document.createElement("iframe");
iframe.style = "display:none !important";
document.body.appendChild(iframe);
var doc = iframe.contentWindow.document;
var htmlStr = `
    <html><head><title></title></head><body>
    <script src="./divide.js"></script></body></html>
`;
ドキュメントを開きます。
doc.write(htmlStr);
ドキュメントを閉じる();

この方法では、モジュール スクリプトを「分離されたスコープ」に読み込むことができます。しかし、明らかにまだ正常に動作していないため、次のステップはモジュールのインポートおよびエクスポート機能を完成させることです。モジュールのエクスポートで解決する必要がある問題は、メイン ウィンドウがサブウィンドウ内のモジュール オブジェクトにアクセスできるようにすることです。したがって、子ウィンドウのスクリプトが読み込まれて実行された後、メイン ウィンドウの変数にマウントできます。

上記のコードを変更します。

// ...繰り返しコードを省略 var htmlStr = `
    <html><head><title></title></head></body>
    <スクリプト>
        親ウィンドウのプロパティをオーバーライドします。
        window.exports = window.module.exports = 未定義;
    </スクリプト>
    <script src="./divide.js"></script>
    <スクリプト>
        if (window.module.exports !== 未定義) {
            親ウィンドウのmodObj['./divide.js'] = ウィンドウのモジュールをエクスポートします。
        }
    </スクリプト>
    </body></html>
`;
// ...重複コードを省略

核となるのは、parent.window などのメソッドを通じて、メイン ウィンドウと子ウィンドウ間の「浸透」を実現することです。

  • サブウィンドウのオブジェクトをメインウィンドウにマウントします。
  • メインウィンドウのメソッドを呼び出すサブウィンドウの機能もサポートしています。

上記は、原理の大まかな実装にすぎません。より厳密な実装の詳細に興味がある場合は、ソース コードの loadModuleForModuleData メソッドを参照してください。

「3.1. 依存関係分析」では、依存関係を取得するためにすべてのモジュールを一度ロードする必要があると述べられていますが、この部分のロードも iframe で実行され、「汚染」を防ぐ必要があることも言及する価値があります。

3.3. モジュールランタイムの提供

モジュールのランタイム バージョンには、モジュール オブジェクトの構築、モジュール オブジェクトの保存、およびモジュールのインポート メソッド (require) の提供が含まれます。モジュール ランタイムのさまざまな実装は、一般的に似ています。ここで注意する必要があるのは、分離メソッドが iframe を使用する場合、いくつかのランタイム メソッドとオブジェクトをメイン ウィンドウとサブウィンドウ間で渡す必要があることです。

もちろん、詳細には、モジュール パスの解決、循環依存の処理、エラー処理などのサポートも必要になる場合があります。この部分の実装は多くのライブラリと同様であったり、特にコアではないため、ここでは詳しく紹介しません。

4. 結論

最後に、一般的な操作プロセスをまとめます。

1. まず、ページからエントリ モジュールを取得します。one-click.js では、document.querySelector("script[data-main]").dataset.main; です。

2. iframe にモジュールをロードし、新しい依存関係が現れなくなるまで require メソッドでモジュールの依存関係を収集します。

3. コレクションが完了すると、完全な依存関係グラフが取得されます。

4. 依存関係グラフに従って、対応するモジュール ファイルを「逆」に読み込み、iframe を使用してスコープを分離し、メイン ウィンドウのモジュール ランタイムを各サブ ウィンドウに渡すことに注意を払います。

5. 最後に、エントリ スクリプトがロードされると、すべての依存関係が準備され、直接実行できるようになります。

一般的に、ビルド ツールとサーバーの助けがなければ、依存関係の分析とスコープの分離を実装することは困難です。 One-click.js は、上記の技術的な手段を使用してこれらの問題を解決します。

では、one-click.js は本番環境で使用できるのでしょうか?明らかにそうではありません。

これを本番環境で使用しないでください。このユーティリティの唯一の目的は、ローカル開発を簡素化することです。

したがって注意してください。著者は、このライブラリの目的は地域の開発を促進することだけであるとも述べています。もちろん、これらの技術的な手段のいくつかを学習教材として学ぶこともできます。興味のある方は、one-click.js リポジトリにアクセスして詳細を確認してください。

上記は、コンパイル/サーバーなしでブラウザでCommonJSモジュール化を実装する方法の詳細です。コンパイル/サーバーなしでCommonJSモジュール化を実装する方法の詳細については、123WORDPRESS.COMの他の関連記事に注目してください。

以下もご興味があるかもしれません:
  • webpack を使用して CommonJS と ES モジュールの違いを理解する
  • CommonJS の Node モジュール仕様
  • CommonJSモジュールの読み込みを理解する
  • ES6 と CommonJS のモジュール処理の違い
  • commonjsモジュールとes6モジュールの違いについての詳細な説明
  • Commonjs仕様とNodeモジュール実装の深い理解
  • Browserify を使用して CommonJS ブラウザ読み込みメソッドを実装する
  • 非常に便利なJavaScriptパッケージングおよび圧縮ツールを共有する
  • webpackを使用してJSをパッケージ化する方法の詳細な説明

<<:  MySQLインデックス最適化分析に関する簡単な説明

>>:  Linux で同じ内容のファイルを識別する方法の詳細な説明

推薦する

ApacheとTomcatによるクラスタ環境構築プロセスの分析

実際、Apacheクラスタを構築するのは難しくありません。私もインターネットで情報を見つけて自分で設...

フィールドを結合するSQL関数

最近、関連テーブル内のすべてのフィールドをクエリし、それらを 1 つのフィールドに再グループ化する必...

Linux でディスク IO を表示し、読み取りと書き込みで高い IO を占有するプロセスを見つけます。

背景 - オンラインアラートオンライン サーバーがアラームを発し、ディスク使用率 disk.util...

Vue開発の一般的な手法の詳細な説明

目次$nextTick() $forceUpdate() $セット() .sync——2.3.0 以...

MySQLサブクエリでorder byが効かない問題の解決方法

偶然にも、SQL ステートメントを異なる MySQL インスタンスで実行すると、異なる結果が生成され...

CentOS7 に MySQL をオフラインでインストールする詳細なチュートリアル

1. 元のmariadbを削除します。削除しないとmysqlをインストールできません。 mariad...

Linux 名前空間ユーザーの詳細な説明

ユーザー名前空間は Linux 3.8 で追加された新しい名前空間で、ユーザー ID やグループ I...

MySQL で単一のフィールド内の複数の値を分割および結合する方法

複数の値を組み合わせて表示これで、図1から図2に示す要件が揃いました。 どうやってやるんですか?次の...

MySQLデータベースの増分バックアップのアイデアと方法

MySQL データベースの増分バックアップを実行するには、データベース構成ファイル /etc/my....

Prometheus+Grafanaによるnginxの監視方法を分析する

目次1. ダウンロード2. nginxとnginx-vts-exporterをインストールする3. ...

Vueカスタムコンポーネントは双方向バインディングを実装します

シナリオ:一般的に使用される親コンポーネントと子コンポーネント間の相互作用方法は次のとおりです。親コ...

WindowsシステムでPhPStudy MySQLの起動に失敗する問題を解決する

エラーを報告するApache\Nginx サービスは正常に起動しましたが、MySQL は起動に失敗し...

JavaScriptはPromiseを使用して複数の繰り返しリクエストを処理します

1. なぜこの記事を書くのですか?重複リクエストの処理に関する記事をたくさん読んだことがあるでしょう...

MySQLパーティションテーブルは月別に分類されています

目次テーブルを作成するデータベース ファイルを表示します。入れるクエリ消去補足:Mysqlは月テーブ...

Linux システムの仮想ホストで Swoole Loader 拡張機能を有効にする方法

特記事項: Swoole 拡張機能のみがインストールされ、サーバーはホストにインストールされません。...