Dynamic Imports の使用で ES Modules における AdBlock ツールの影響を最小限に

ES Modules はブラウザのサポート状況(Can I use...)も良好であり、 Internet Explorer のサポートが不要であれば実サイトに導入することが可能です。このブログで読み込んでいるスクリプトも2018年にモジュール化をしています。

しかし、モジュール化で便利になったからと何でもかんでも import していたところ、特定のブラウザですべてのスクリプトがまったく動かないという事象に遭遇しました。原因を調べてみると、どうも AdBlock ツールが影響していたようで、その概要と対策方法を書き記しておきます。

例えば 'ads' が含まれる URL をブロックするようにしている場合、以下のスクリプトは動作しません。

<script type="module">
import Hoge from './Hoge.js';
const hoge = new Hoge();

import Ads from './Ads.js';
const ads = new Ads();
</script>
  • Firefox 83 にアドオン uBlock Origin(GitHub) をインストールした状態で確認。また、 Brave(brave.com) のようにデフォルト設定のままでブロックされるブラウザも存在します。

この場合、 Ads.js だけでなく、広告とはまったく関係のない Hoge.js も実行されません。

ただし、 Hoge.js の読み込みは行われ、サーバーのアクセスログにはステータスコード 200 で記録されるので、 Hoge.js がブラウザで実行されていないことをサーバー側で検知するのは困難でしょう。

JS ファイル アクセス ブラウザ内での実行
Hoge.js
Ads.js

広告がブロックされるだけならまだしも、その他の機能まで動かなくなってしまうのは困りものですが、いくつか解決策はあります。

要するにスコープを分けてしまえばいいので、気分的には即時関数を使ってこうしたいところです。

<!-- ※これは悪い例です。実際には動作しません。 -->
<script type="module">
(() => {
  import Hoge from './Hoge.js';
  const hoge = new Hoge();
})();

(() => {
  import Ads from './Ads.js';
  const ads = new Ads();
})();
</script>

しかし、静的インポートはトップレベルでしか使えないので、これは構文的に NG です。

代わりに <script> 要素を分離することで同じようなことができます。

<script type="module">
import Hoge from './Hoge.js';
const hoge = new Hoge();
</script>
<script type="module">
import Ads from './Ads.js';
const ads = new Ads();
</script>

しかし、 AdBlock 対策のためだけに HTML 要素に手を入れるのはあまりやりたくないですよね。

そこで Dynamic Imports (動的インポート)の出番です。これを使うことで、 JavaScript 内だけの変更で対処できます。分かりやすく随所に console.log() を挿入したコード例を以下に示します。

<script type="module">
console.log('module start');

import Hoge from './Hoge.js';
const hoge = new Hoge();

(async () => {
  console.log('async start');

  const Ads = await import('./Ads.js');
  const ads = new Ads().default();

  console.log('async end');
})();

console.log('module end');
</script>

この場合、コンソールには

  1. module start
  2. async start
  3. module end

の3つが出力されます。 await import('./Ads.js') の行で処理が止まってしまうため、 async end は出力されません。

また、動的インポートはトップレベル以外でも使えるため、 try - catch で囲み、ブロッキングされた事実を fetch() でサーバーに送ったり、あるいは代替処理を書いたりすることもできます。

<script type="module">
  import Hoge from './Hoge.js';
  const hoge = new Hoge();

  (async () => {
    try {
      const Ads = await import('./Ads.js');
      const ads = new Ads().default();
    } catch (e) {
      /* Ads.js が読み込まれなかった場合の処理 */
    }
  })();
</script>

デメリットといえば、静的インポートと比べて少しだけ新しい技術のため、 Edge Legacy が対応していないところでしょうか。