Parcelがシンプルで楽すぎた

とある作業をしているときに、npmパッケージを使いたいけどいちいちwebpack.config.jsを書くのは面倒だなーという自堕落が発動したので試しにParcelを使ってみたところすごく楽で良かったので紹介します。

Parcel

f:id:ponday:20180916142614p:plain

公式サイト

WebpackやRollup.js、FuseBoxなんかと同じモジュールバンドラーです。ロゴにも書いてあるとおりビルドの速さとzero configuration = 設定ファイルなしで動作するという点を売りにしているらしいですね。

リリースは2017/12月ごろですが、既にGitHubのスター数は26,000を超えていて、注目度の高いプロジェクトです。

モジュールバンドラーの面倒さ

モジュールバンドラーはすごく便利なツールですが、導入するためにはまずビルドの設定ファイルを準備しなければいけません。例えばWebpackならば、webpack.config.jsには入出力ファイル、拡張子ごとのビルド設定やビルドオプションなどを指定する必要があります。

詳細なオプションを備えているからこそ様々な用途に利用できるという面はもちろんあるのですが、ボイラープレート的な部分も多く、バージョンアップ毎に設定ファイルの書き方がわずかに変更されることも多いので、ライトユースには面倒に感じる場面があります。

対してParcelは面倒な設定部分を極力省略してビルドができるようになっており、この辺りの面倒さを解消してくれています。

触ってみる

ParcelはWebpackなどと同様、npmパッケージとして提供されています。 公式を見るとグローバルにインストールするよう記載がありますがnpxでも動作するので、とりあえず試したい場合はそちらで試しても良いと思います。 今回はnpxを利用していきます。

まずはインストール

npm install --save-dev parcel

Parcelは内部的にBabelを使っているらしいので、Babelのプリセットやプラグインを使う時は.babelrcを用意します。今回は以下のような設定を準備しました。

{
  "presets": [
    ["env", { "modules": false } ]
  ]
}

(ちなみにこの表記、Babelのバージョン7用の@babel/preset-envではなくバージョン6用のbabel-preset-env用のものなのですが、2018/09/17現在ParcelがBabelのバージョン7に対応していないようです。)

ファイルのディレクトリ構成は以下のようにしました。

root
├─ dist
│
├─ src
│   ├─ app.js
│   └─ index.html
│
├─ .babelrc
└─ package.json

モジュールバンドルを試せないことにはどうしようもないので、(わざわざ)RxJSを使います。

npm install --save rxjs

index.htmlは↓

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
    <article id="container"></article>
    <script src="app.js"></script>
</body>
</html>

index.jsは↓

import { map, concatMap, reduce } from 'rxjs/operators';
import { ajax } from 'rxjs/ajax';

window.addEventListener('DOMContentLoaded', () => {
    const url = 'https://accounts.google.com/.well-known/openid-configuration';
    ajax(url).pipe(
        concatMap(({ response }) => Object.entries(response)),
        map(([key, value]) => createElement(key, value)),
        reduce((df, element) => {
            df.appendChild(element);
            return df;
        }, document.createDocumentFragment())
    ).subscribe(df => {
        document.getElementById('container').appendChild(df);
    });
});

function createElement(key, value) {
    const section = document.createElement('section');
    const h2 = document.createElement('h2');
    h2.textContent = key;
    section.appendChild(h2);

    const createContent = Array.isArray(value) ? createContentForArray : createContentForValue;
    section.appendChild(createContent(value));

    return section;
}

function createContentForArray(value) {
    const ul = document.createElement('ul');
    value.forEach(v => {
        const li = document.createElement('li');
        li.textContent = v;
        ul.appendChild(li);
    });
    return ul;
}

function createContentForValue(value) {
    const p = document.createElement('p');
    p.textContent = value;
    return p;
}

それでは、Parcelでビルドをしてみます。

npx parcel src/index.html

上記コマンドを実行して、http://localhost:1234/にアクセスするとビルド結果が確認できます。また、ビルド対象ファイルに変更が走ると自動で再ビルドが走ります。

ちなみにこの時はdevelopmentモードなのでビルドされたソースコードのファイルサイズはそれほど小さくないです。

  • index.html : 4KB
  • app.js : 463KB

productionビルドはparcel buildで実行します。

npx parcel build src/index.html -d dist

ビルド結果は以下の通りで、ファイルサイズが圧縮できていることが分かります。

  • index.html : 317B
  • app.js : 201KB

ほとんどパッケージを追加することもなくビルドできてしまいました。楽!

Tree Shakingについて

上記のビルド結果を見た時、「あれ、ビルドサイズってこんなもんだっけ...?」という疑問が浮かびました。RxJSのajaxなんてあまり使わない関数を使ってしまったので、それのサイズが大きいのかとも思いましたがあの関数自体はそれほど大きなものではないはずです。

念のため、Webpackでもapp.jsをビルドしてみました。

  • app.js : 29KB

あれ、小さい...
というより、想像しているサイズ感はこんな感じです。

調べてみたところ、Parcelのv1.9ではTree Shakingは実験的な機能扱いらしく、--experimental-scope-hoistingオプションをつけてビルドする必要があるそうです。

npx parcel build src/index.html -d dist --experimental-scope-hoisting

上記の結果は以下の通り。

  • app.js : 28KB

小さい!!!
求めている結果はコレですね。まだ実験的な機能扱いなので、早めに本実装されて欲しいところです。

まとめ

  • ParcelはWebpackなどと同じモジュールバンドラー
  • 設定ファイルなしでビルドができる
  • とにかく楽でライトユースにやさしい
  • Tree Shakingは実験的サポート。本実装が待たれる

Parcelは何よりそのシンプルさが魅力的です。

触ってみた印象では、多機能で柔軟なWebpackに対して、シンプルで簡単なParcelと言った具合で、良い住み分け、使い分けができそうなツールという感じでした。

例えば新しいライブラリを使いたいとき、細かくビルド設定を整えるより先にまずは色々と試してみたいといったニーズには良いツールではないでしょうか。

ざっくりWeb Components

今年の2月、Fukuoka Engineers Day 2018というイベントで「Web Componentsの現在地」というタイトルで発表しました。(まとめはこちら

そこから半年、Firefoxでもバージョン63でついにすべての仕様がサポートされるらしいので、あらためてWeb Componentsの仕様のおさらいメモ。

Web Componentsとは

Web ComponentsはWeb標準の技術のみで再利用可能な部品を作るためのWeb APIです。JSフレームワークを導入して実現していた独自のコンポーネントを標準APIのみで実現できるようになります。

(とはいえ、Web ComponentsとJSフレームワークは競合する考え方ではなく、組み合わせて利用可能なものです。Web Componentsが使えるようになるからJSフレームワークは不要になる!という類のものではないです。念のため。)

Web Componentsそのものが仕様を指すわけではなく、

  • HTML templates
  • Custom Elements
  • Shadow DOM

の3つの仕様をまとめてそう呼んでいます。

HTML templates

HTMLのtemplateタグのことです。ページの初期表示時にはレンダリングされず、必要に応じてJavaScriptから操作してDOMに追加します。

<template>
  <h1>HTML templates Demo
</template>

<div id="container"></div>
const template = document.getElementById('demo').content;
const container = document.getElementById('container');

const node = template.cloneNode(true);
container.appendChild(node);

Custom Elements

HTMLに独自のタグを追加するAPIです。customElements.defineを使って定義します。

Shadow DOM

グローバルなDOMツリーから分離された、カスタム要素に閉じたDOMツリーを持つことができます。CSSなど、グローバルに影響を与えずに、カスタム要素内で独自にスタイリングしたり、処理を行ったりできるようになります。

対応ブラウザ

今のところ、上記の仕様をすべて実装している(=既にWeb Componentsが使える)ブラウザはGoogle ChromeSafari(デスクトップ、モバイル)です。Firefoxはバージョン63で対応予定、EdgeはHTML templatesのみ対応、IEは全ての仕様に未対応です。

課題はEdgeとIEですが、webcomponents.jsというPolyfillを利用することでShadow DOM以外の仕様をエミュレートすることができます。(Shadow DOMはパフォーマンスの問題でoffになっています。)

とはいえShadow DOMはすごく魅力的な仕様なので、これが使えないのは惜しいと言わざるを得ないです。(実際の動作はこの後のサンプルを確認して下さい。)

書いてみる

最小のサンプル

とりあえず最小のサンプルです。

class MyElement extends HTMLElement {
    connectedCallback() {
        const shadowRoot = this.attachShadow({ mode: 'open' });

        const h1 = document.createElement('h1');
        h1.textContent = 'Web Components Test';

        shadowRoot.appendChild(h1);
    }
}
customElements.define('my-element', MyElement);
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
    <script src="index.js"></script>
</head>
<body>
    <my-element></my-element>
</body>
</html>

上記をそれぞれindex.html, index.jsと言う名前で保存して開いてみてください。(当然ですがWeb Componentsの仕様に対応したブラウザで確認して下さい)

手順としてはHTMLElementを継承したクラスを定義して、connectedCallbackイベントでShadow DOMを有効化(attachShadow({ mode: 'open' }))し、通常のDOM操作同様にappendChildして要素を追加する。定義したクラスをcustomElements.defineに渡してCustom Elementsを定義する、という流れです。

Shadow DOMによるScoped CSS

Shadow DOMによってCSSの影響が分離されていることを確認してみます。JavaScriptはそのままで、index.htmlを以下のように変更します。

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
    <script src="index.js"></script>
    <style>
        h1 {
            color: red;
        }
    </style>
</head>
<body>
    <my-element></my-element>
    <h1>DOM Tree</h1>
</body>
</html>

このHTMLを表示すると、以下のように表示されます。

f:id:ponday:20180915223245p:plain

通常のh1要素は文字色が変更されていますが、カスタム要素のh1要素には効いていないことが分かります。

これは逆も同様です。

class MyElement extends HTMLElement {
    connectedCallback() {
        const shadowRoot = this.attachShadow({ mode: 'open' });

        const styles = document.createElement('style');
        styles.textContent = 
`
h1 {
    color: red;
}
`;
        shadowRoot.appendChild(styles);

        const h1 = document.createElement('h1');
        h1.textContent = 'Web Components';
        shadowRoot.appendChild(h1);
    }
}
customElements.define('my-element', MyElement);
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
    <script src="index.js"></script>
</head>
<body>
    <h1>DOM Tree</h1>
    <my-element></my-element>
</body>
</html>

このように変更して結果を確認すると以下のようになります。

f:id:ponday:20180915223929p:plain

見ての通り、h1タグを指定してスタイルを適用しても、カスタム要素の外には影響がありません。

ChromeのDeveloper Toolで確認すると以下のようにレンダリングされています。

f:id:ponday:20180915224859p:plain

このように、styleタグがshadow-rootの中にレンダリングされており、影響範囲がカプセル化できていることが分かります。

まとめ

  • Web ComponentsはWeb標準APIを利用して再利用可能な部品を作るための仕組み
  • Web Components自体がAPIを指すのではなく、いくつかの仕様の総称
  • そろそろWeb ComponentsがFirefoxでも使えるように!
  • Shadow DOMによってWeb標準の要素によってScoped CSSが使える未来も近い
  • Edgeは実装はよして
  • IEはサポート打ち切りはよして

Web ComponentsとJSフレームワークについては別の記事で書ければ。では。