JavaScriptを劇的に教えやすくするWebサーバ作りました

  • 957
    Like
  • 4
    Comment
More than 1 year has passed since last update.

近年、JavaScriptでコードを書こうとすると、お膳立て7割、コード書き3割みたいな事態がざらにあります。「お膳立て」の例としては、Gulp, Babel, Sass, PostCSS, WebPack, Rollup, Browserifyほか数限りなく。

たしかに、一旦フロントエンド開発に慣れてしまえば、お膳立てにかける時間は短縮することが可能です。でも、これを初学者に強いるのはツラすぎる...! 覚える方はともかく、 教える側がツライ

今回、未来なJavaScriptやCSSの文法で書いても、よしなにとりなしてくれるWebサーバ「Felt」を作ったので、ご査収ください。

(2016/7/19 関連ツールとの比較を追記しました)

logo.png

手軽さを取り戻す

2016年現在の典型的なフロントエンドの作業フローは、こんな感じだと思います。

  1. ES6でプログラムを書く
  2. 依存するモジュールを含めて1ファイルに (バンドル)
  3. ES5にバベる (Babel)
  4. サーバにデプロイする

これが、Feltを使うとこうなります。

  1. ES6でプログラムを書く
  2. サーバを起動する

以上。コマンドとしても、インストール + サーバ起動でこれだけ。

$ npm i -g felt felt-recipe-minimal
$ cd path/to/project
$ felt

http://localhost:3000/にアクセスすると、コンパイル済みの状態でサイトが見られます。

実際は何しているのか

Feltの実態は、Expressのミドルウェアとして、各種トランスパイラを扱えるようにしたラッパーライブラリです。JavaScriptの処理にRollup、CSSの処理にPostCSSを使った場合、次の図のような構成になります。

system@1x.png

CLIから起動した場合は、Expressのstaticサーバを兼ねるので、superstaticなどのような、静的コンテンツサーバっぽく使うことができます。コンパイルの処理が、数百ミリ秒〜数秒かかってしまうため、Webサーバ起動時にまとめてコンパイルし、あとは変更のあったファイルだけ再コンパイルしています(--watchオプション指定時)。

ハンドラー

Feltでは、サーバにリクエストを受けた時、拡張子ごとに処理を振り分けます。.js.cssといった拡張子ごとに、コンパイルの方法を指定します。(そういえば、コンセプト的にApacheの設定に近いものがありました)

未指定の拡張子のファイルについては、そのままコンパイルなしで出力されます。

プラグイン

ハンドラーに直接処理を書いても良いのですが、通常はRollupやPostCSS用のプラグインを使います。現在のところ公式で用意しているプラグインは次の2つです。

CLIオプション

最初の例のように、オプションなしでも起動できますが、通常はいくつかのオプションを組み合わせます。

$ felt --recipe standard --src public

以下は、主なオプションについての説明です。

オプション デフォルトの値 備考
--src . ドキュメントルート (公開フォルダ)
--cache cache キャッシュファイルを置くフォルダ
--port 3000 Webサーバのポート
--watch n/a セットするとファイルの更新を自動検知
--debug n/a セットするとデバッグコメントをターミナルに出力
--recipe なし recipeとconfigの両方とも設定がない場合、minimalがセットされる
--config なし ファイル名を省略した場合、felt.config.js
--export なし セットした場合、そのディレクトリにコンパイル済みのファイルを出力

ファイル監視

$ felt --recipe standard --src public --watch

このようにすると、publicフォルダの内容を監視し、変更を検知すると自動的にコンパイルして結果をキャッシュします。

エクスポート

$ felt --recipe standard --src public --export dist

このようにすると、publicフォルダの中身が、そのままdistにコピーされ、そのうちコンパイルが必要なものについては、コンパイル済みの内容に置き換えられます。

開発中は、Feltで確認し、本番はS3やGitHub Pagesにデプロイするといった使い方をする場合に有用です。

レシピと設定

RollupやPostCSSの設定は「レシピ」や「コンフィグ」という形で残すことができます。

  • レシピ: npmにモジュールとして公開されているもの。felt-recipe-*
  • コンフィグ: ファイルfelt.config.jsに設定を書いたもの
  • 実行時に指定するオプション: レシピやコンフィグに、--src publicなどとして上書き

priority@1x.png

レシピとコンフィグは、基本的には同じ書式です。npmに公開されているかだけが異なります。rollupの部分は、Rollupの設定ファイルそのものですね。実際、felt-rollupというプラグインが使われているほかは、Rollupのプラグインが直接使われています。

felt.config.js

const
  rollup = require('felt-rollup'),
  buble = require('rollup-plugin-buble'),
  resolve = require('rollup-plugin-node-resolve'),
  commonjs = require('rollup-plugin-commonjs')

module.exports = {
  src: 'public',
  handlers: {
    // 拡張子ごとに、コンパイルの方法を指定する
    '.js': rollup({
      plugins: [
        resolve({ jsnext: true }),
        commonjs(),
        buble()
      ],
      sourceMap: true
    })
  }
}

すでにRollupの設定があれば、そのファイルを流用しても構いません。

const rollup = require('felt-rollup')

module.exports = {
  src: 'public',
  handlers: {
    '.js': rollup(require('./rollup.config.js'))
  }
}

書き方については、公式レシピ(↓)を参考にしてください。

公式レシピ

まとめ

GulpにしろWebPackにしろ、配信ディレクトリと別の場所にコードを置くと、パスの変更だけでもなかなかに面倒です。Feltはその点、 パスの書き換えが一切いらない のが嬉しいポイント。

Feltを サーバサイドで実現するPolyfill と捉えることもできます。今後ブラウザの実装が進めば、そのままFeltを外して運用できるようになるかもしれません。コードはあくまでも 将来の理想形 で書き、サーバで送出する際に調整(polyfill)を入れるというのが、Feltの考え方です。

もし、使えるなと思ってもらえたら、プルリクエストをぜひ。不明点は、GitHubのissueに日本語で書き込んでください。英文修正も歓迎です :smile_cat:

おすすめの使い方

  • JavaScriptの学習に: Feltであれば、最初は公式レシピを使ってコンパイルについては気にせずにサイトを作ることができます。学習が進むにつれて、コンフィグをいじれば高度なこともできます。GulpやWebPackなどにその時点で乗り換えるのもありでしょう。
  • 小規模プロジェクトに: とりあえずサイトを作る場合、package.jsonnode_modulesの管理まで入ってくると面倒な場合もあります。そんなときは、さくっと書いて、Feltでさくっと公開。(あるいは、エクスポートしてしまう)
  • SPA (single page app): API部分をNodeで書いているなら、そこに追加する形でFeltを入れれば、OK。

Appendix

補足1: 関連ツールとの比較

Feltに似た機能を提供するツールはいくつか存在します。立ち位置をわかりやすくするため、それらとの比較をしておきたいと思います。「Harp/WebPackと何が違うの?」という話です。主なポイントを2つ挙げると、次のようになるでしょう。

  • 次世代標準に準拠するのが主目的
  • パスの書き換えを行わない

後者は、大したことではないように感じるかもしれませんが、前者の実現のためには重要な点です。(学習の負荷軽減という点でも)

以下、もう少し掘り下げてみます。Harpはimport/export文に対応する(バンドル)機能がありません。CoffeeScriptのコンパイルはできますが、Babelとかの文脈ではない点に注意が必要です。また、テンプレート対応のためにinclude文が使えるようになっていますが、Web標準から大きく離れています。どちらかというと、HammerCactusの延長にあるものと捉えられます。

一方WebPackは、強力なバンドル機能を持ちます。が、強力すぎて標準から大きく乖離していくきらいがあります。TypeScriptやCoffeScriptにとどまらずCSSや画像ファイルまでも、バンドル対象にできてしまうのです。便利な反面、初学者泣かせなところでもあります。開発用サーバに特化したHot reloadingなども人気ですが、Browsersyncの方が便利な面もあり、悩ましいところです。

SuperstaticやBrowsersyncは、サーバ機能を持つという点で比較に含めましたが、コンパイルについては別ツールとの連携が前提になります。

名称 説明 サーバ
機能
コンパイル
機能
バンドル
機能
ファイル
監視
他の機能
Felt 次世代標準をサーバサイドでPolyfillするWebサーバ ブラウザごとの切替 (実装中...)
Harp デザイナー視点でサイト制作を簡単にするフレームワーク × JadeやMarkdown対応と、別ファイルのインポート
WebPack あらゆるファイルをPackするバンドラー 強力な開発用サーバ、コード分割ほか
Superstatic シンプルな静的コンテンツサーバ × × ×
Browsersync 開発用サーバの定番 × × ライブリロード、複数台の画面同期

※注: 表中、WebPackとBrowsersyncの「サーバ機能」については、開発用途に限定されるという意味で「△」としています。

その他、視野に含めるべきはSystemJSです。ブラウザ単体でのモジュールバンドルを実現するツールで注目が集まっていますが、多数の依存を持つライブラリを本当にこの方法でバンドルすることが現実的なのか、筆者としてはまだ自信が持てずにいます(HTTP/2が普及したとしても)。同じ理由でjspmも失速しているように見えます。とはいえnpmも万全ではなく、CommonJS vs ES6の対立が解決する気配を見せないので、うむむむ。

補足2: トランスパイルをめぐる状況

2016年7月初旬、WebKitがES6準拠100%をアナウンスしました。これは、次期「macOS」のSafariとして搭載される予定で、開発者プレビューを見る限りでは、iOS版も100%準拠すると見てよいでしょう。Chrome, Edge, Firefoxもすべて90%を超えた今、Safariさえどうにかなればコード変換なしでもES6が動く未来は近い...、はず。

ただ、実際のところとしては、ES6非対応ブラウザは依然として残ります。一方で、議論の軸はすでにES7以降に移っているため、すでにasync/awaitを使い出している開発者も一定数います。つまり、

  • 過去への互換のためのコード変換
  • 未来を先取りするためのコード変換

の2つが、今後も混在し続けると考えるべきでしょう。しかし、現状のソリューションのではブラウザによってコンパイル方法を変えることができません。これは、最新ブラウザにとって大きなオーバーヘッドです。

幸い、Feltの実装方法であればこの点は、簡単に改良が可能です。今後、追加で開発していきたい部分です。

補足3: Expressのミドルウェアとして使う

Feltは、CLIとして使うほか、Expressのミドルウェアとして使うこともできます。APIサーバがすでにNodeで構成されている場合は、そこにFeltを追加するのも選択肢です。

$ npm install --save felt

server.jsはこんな感じになります。

const
  express = require('express'),
  felt = require('felt'),
  recipe = require('felt-recipe-minimal')

const app = express()

app.use(felt(recipe, { src: 'public' }))
app.use(express.static('public'))
app.listen(3000)

先ほどのコンフィグファイルを使う場合はこんな感じ。

const
  express = require('express'),
  felt = require('felt'),
  config = require('./felt.config.js')

const app = express()

app.use(felt(config, { src: 'public' }))
app.use(express.static('public'))
app.listen(3000)

なお、felt()は複数の設定を引数として受け取ることができます。

felt(config1, config2, config3)

この場合、config1, config2, config3と順番に設定が合成されます。(config3の内容が優先)