Webpack を使っていてファイルの相対パスを書くのがつらくなったとき

小ネタ。

Webpack(というよりモジュールバンドラ) を使っていて、ディレクトリの階層が深くなってくると import や require でロードするファイルのパスを ../../../../foo.js のように ../ の数を正確に指定するゲームになってくる。

以下のように書くことで src ディレクトリをルートにしてパスを指定することができるようになるのだけど

resolve: {
    modules: [
      path.resolve(__dirname, 'src'),
      "node_modules"
    ],
},

同僚氏に npm でいれたパッケージか、src にあるファイルか分からないから、src ディレクトリの alias を定義したらどうか、と勧められた。

resolve: {
    alias: {
        '@': resolve(__dirname, 'src'),
    },
},

これで、どの階層にあるかに関係なく、import store from "@/store" とか import router from "@/router" とか書ける。Vue の Progressive Web App template や Nuxt.js でも同様のことをやっている。最悪、記号の単純な置換か、簡単なスクリプトを書けば済むので、筋は悪くないと思う。

Vue で配列を使った算出プロパティの値が変わらないとき

1回ハマったら体が覚えて、次回から気を付けるようになるのだけど、記事にしておく。

例えば、以下のように配列をスタックとして使い、トップを算出プロパティで定義したとき。1秒後にトップの id が 100 になると思いきやそうはならない。Vue は push, pop, splice といった操作のメソッドをラップして、配列の変更を検知しているので、配列の要素へ代入しても配列の変更とはみなされない。

new Vue({
   data: {
      items: []
   },
   mounted() {
       this.items.push({id: 1});
       this.items.push({id: 2});
       this.items.push({id: 3});
       setTimeout(() => {
           this.items[this.items.length - 1] = {id: 100};
       }, 1000);
   },
   computed: {
      top() {
          if (this.items.length === 0) {
              return null;
          }
          return this.items[this.items.length - 1];
      }
   }
});

これを意図通りに動かすには setTimeout のコールバックの処理を次のように splice メソッドを使って変更するように書き換える。

this.items.splice(this.items.length - 1, 1, {id: 100});

vue-router でパスにマッチしたコンポーネントの Vue インスタンスにアクセスする

タイトルが長い。

お仕事で vue-router 絡みのプラグインを書いていてアクセスしたくなった。

VueRouter のインスタンスが router として、以下のようにプロパティを参照することでできる。

router.currentRoute.matched[0].instances['default']

あくまで内部がこうなっているというだけの話で、API として正式に提供されているものではないので注意。

matched が配列になっているのは、ネストしたルートを定義することができるから。 例えば、以下のような定義の場合、/user/3/profile のようなパスでアクセスしたら、/users/3 で1回、/users/3/profile でもう1回マッチすることになる。

{
  path: '/users/:id',
  component: User,
  children: [
    {
      path: 'profile',
      component: UserProfile
    },
  ],
}

instances がオブジェクトになっているのは名前付きビューがあるため。以下のようなルーティング定義をした場合、instances オブジェクトの default, a, b それぞれのプロパティでコンポーネントの Vue インスタンスにアクセスできる。名前付きビューを使っていない場合は default プロパティを参照すれば良いです。

[
  {
    path: '/',
    components: {
      default: Foo,
      a: Bar,
      b: Baz
    },
  },
]

(keepalive コンポーネントを使うと話が変わりそうですが)インスタンスの参照はいつ変更される分からないので、参照を変数に入れて、使いまわすとかはしないほうがよさそう。

ServiceWorker で、ネットワークから取得しようして失敗したらキャッシュにフォールバックするやつ

ここ最近、ServiceWorker を使って色々試しているのだけど、その中のひとつ。

以下の内容を、エントリーの HTML や参照系の API に適用して、アプリケーションのオフライン対応を実現する。通常はネットワークから取得した最新の内容を使うが、オフラインやネットワークが不安定な場合はキャッシュの内容を使う。

  • ネットワークから取得しようとして失敗したらキャッシュを使う
  • ネットワークから取得するとき、1秒のタイムアウトを設定する
  • ネットワークから取得しようとして成功したらキャッシュに保存する
  • ネットワークから取得しようとして失敗した場合、キャッシュが存在しなかった場合はログにその旨を出力する

ServiceWorker 内で使える API は Promise を返すものがほとんどなので、Promise を駆使するかんじ。

var CACHE_NAME = 'v1';

var fetchWithTimeout = function(request) {
    return new Promise(function(resolve, reject) {
        setTimeout(reject, 1000);
        return fetch(request).then(resolve);
    });
};

self.addEventListener('fetch', function(event) {
    // 本当はここでパスやヘッダーの内容を見て、キャッシュすべき対象か調べる
    event.respondWith(
        fetchWithTimeout(event.request)
            .then(function(response) {
                return caches.open(CACHE_NAME).then(function(cache) {
                    console.log('Put to cache', response);
                    cache.put(event.request, response.clone());
                    return response;
                });
            })
            .catch(function(e) {
                console.error('Failed to fetch', e);
                return caches.open(CACHE_NAME).then(function(cache) {
                    console.log('Fallback to cache', event.request);
                    var response = cache.match(event.request);
                    if (!response) {
                      console.error('Missing cache', event.request);
                      return;
                    }
                    return response; 
                });
            })
    );
});

puppeteer で Speed Index を算出

puppeteer という Headless Chrome を Node から操作するライブラリが今日発表されたので、以前、記事を書いた WebPagetest を使わずに Speed Index を算出する - kitak's blog を puppeteer でやってみた。

puppeteer の良さ

  • Chrome Developer Tools の開発チームのメンバーが開発・メンテナンスしているので安心
  • API が過不足なく揃っていて、かつ扱いやすい ( chrome-remote-interface 比 )

経緯

  • 会社の仮想環境とか実際のディスプレイがないマシンで Speed Index の計測を継続におこないたい。Synthetic Monitoring したい。Xvfb と格闘するのは嫌なので、Headless Browser を使いたい
  • Chrome の Headless サポートが発表された後に chrome-remote-interface で Headless Chrome を操作して Speed Index の算出を試みたのだけど、何か問題があって出来なかった(何でできなかったかは忘れた)
  • Headless Chrome を操作できる良さそうなライブラリ( puppeteer )が公開されたのでリトライ

Speed Index を算出するコード

こんなかんじ。

const fs = require('fs');
const puppeteer = require('puppeteer');
const speedline = require('speedline');

(async() => {
const browser = await puppeteer.launch();
const filename = 'trace.json';
const page = await browser.newPage();
try {
    await page.tracing.start({path: filename, screenshots: true});
    await page.goto('https://www.google.com'); // 計測したいサイトの URL に置き換えよ
    await page.tracing.stop();
    const results = await speedline(filename);
    console.log('Speed Index value:', results.speedIndex);
} catch (e) {
    console.error(e);
}
browser.close();
fs.unlinkSync(filename);
})();

色々

  • async/await を使っているので以前書いた記事のコードより読みやすい。特別、async/await で書くことにこだわりはないのだけど、puppeteer のリポジトリのサンプルコードが async/await で基本書かれているのでそれに倣った
  • 意識が低いのでファイル周りで同期 API を使っていたりするが、そこはもっと良くできそう
  • モバイル端末の viewport をエミュレートして計測したい場合は await page.emulate(devices['iPhone 6']) を足すとできる
  • Web クライアントサイドのパフォーマンスメトリクス — Speed Index、Paint Timing、TTI etc... ::ハブろぐ に書かれた Speed Index 以外のパフォーマンスメトリクスの計測もスクリプトを流し込めばできそうな気がする

その他、オートメーションの API も揃っていて、ServiceWorker のように Request/Response のインターセプトもできる。puppeteer、ページのスクレイピング、プリレンダリング、オートメーションと様々なシーンで利用できそうなおすすめのライブラリです。

DOM 要素が可視状態か調べる

小ネタ。

要素が可視状態かどうか調べたくなって、そういや jQuery に :visible ってセレクタがあったな、と思い出し、おもむろにコードを読んでみた。

結論、こんなかんじで調べることができる( jquery/hiddenVisibleSelectors.js at 2d4f53416e5f74fa98e0c1d66b6f3c285a12f0ce · jquery/jquery · GitHub )。

!!( elem.offsetWidth || elem.offsetHeight || elem.getClientRects().length);

DOM の絡んだユニットテストとかで使えそうですね。

jQuery、以下の理由で新規で採用することは少なくなっていますし、jQuery 不要論の記事も定期的にみかけますが、ちょっと込み入ったことをやる必要が出た時の「大体のブラウザで動く実装例」として捉えると、かなり有用な気がすると思ったのでした。

  • ブラウザ毎のAPIの差異がなくなりつつある
  • DOM API が昔に比べて充実しており、DOM API を直接使うことに対する抵抗感も減ってきている (複雑な DOM 操作が要求される場合は React や Vue などの View ライブラリで対応できる)
  • npm のエコシステムにおいて「一つのことをうまくやる」パッケージが好まれる (モジュールバンドラを使って最終的に生成されるファイルのサイズが小さく収まるという点でもメリットがある)

Vue.js の scoped slot の理解

毎回混乱して、ドキュメントを読んでいるのでまとめ。

以下のようなスロットの機能を利用したコンポーネント child で考える。

<div>
  {{ foo }}
  <child>
     <span>{{ bar }}</span>
  </child>
  {{ baz }} 
</div>

https://jp.vuejs.org/v2/guide/components.html#コンパイルスコープ に書いてある話ですが、上記のテンプレートの foo, bar, baz のスコープは、そのテンプレートを扱っているコンポーネントになる。なんとなく、bar を展開している場所のスコープは child のような気がしてくるのだけど、そうではない。

bar を展開している場所で child のスコープを扱いたい( child のステートで class の付け替えをしたり )場合は、以前までは、child のステートを親のコンポーネントに渡し、親のコンポーネントを通してステートを展開していた。これは、親と子のコンポーネントが密になるのでよろしくない。

この問題を解決するのが scoped slot。child 内の slot コンポーネントに渡したデータが scope 属性値のオブジェクトを通して参照できるようになる。

<!-- child のテンプレート -->
<div>
  <scope :bar="..."></scope>
</div>
<div>
  {{ foo }}
  <child>
     <template scope="props">
       <span>{{ props.bar }}</span>
     </template>
  </child>
  {{ baz }} 
</div>

コンポーネントの親子間でプロパティを通してデータを渡すイメージに近く、属性値の scope の名は props と付けることが多い気がする( ドキュメントもそうなっている )。