Introduction

HyperApp - GitHub

HyperApp is a JavaScript library for building frontend applications.

HyperAppはWebアプリケーションのフロント(主にView)を担うJavaScript用のライブラリです。このライブラリは3つのコンセプトで成り立っています。

  • 外部ライブラリに依存しない、超軽量1KBのライブラリ
  • ステートレスコンポーネント
  • Elm Architecture に則ったスケーラブル可能な仕組み

今回はHyperAppのWiki及び周辺のドキュメント1を勝手に意訳2してみようと思います😌💡

Getting Started

HyperAppは器用に作られたもので、新規プロダクトに取り入れることはもちろん、既存のWebアプリケーションに組み込むこともできるよ。

一番簡単な方法は何と言ってもCDNだね。

<script src="https://unpkg.com/hyperapp"></script>

バージョンを指定したいかな? そんなときはこう。

<script src="https://unpkg.com/hyperapp@0.7.0"></script>

こんにちは世界

ではさっそくHyperAppを使って「こんにちは世界」を表示させてみよう。index.htmlを用意して以下のコードをコピペしてブラウザで見てみよう😄
HyperAppではJSXをつかった記法と、ES6のテンプレートリテラルを使ったHyperxという記法の2つが使えるよ。How wonderful!! それぞれのコード例を用意したから好きな方を使ってね。

JSXで書いた場合
<body>
  <script src="https://unpkg.com/hyperapp"></script>
  <script src="https://unpkg.com/babel-standalone"></script>
  <script type="text/babel">

  const { h, app } = hyperapp
  /** @jsx h */

  app({
    model: "こんにちは世界",
    view: model => <h1 id="title">{model}</h1>
  })

  </script>
</body>
Hyperxで書いた場合
<body>
  <script src="https://unpkg.com/hyperapp"></script>
  <script src="https://wzrd.in/standalone/hyperx"></script>
  <script>

  const { h, app } = hyperapp
  const html = hyperx(h)

  app({
    model: "こんにちは世界",
    view: model => html`<h1>${model}</h1>`
  })

  </script>
</body>

さて、ブラウザでは何が起きたかな?
ブラウザはHyperxやJSXをCDN経由でダウンロードし、scriptの部分をコンパイルして描画したよ。

この例ではHyperAppとは何かを手軽に知ることができたと思うけれど、これだけだとWebアプリケーション開発の例としては少々物足りないよね。わかるよ。

では、次にWebpack、Browserify、Rollupをつかったビルド環境のセットアップの例を見てみよう。

Build Setup

Webアプリケーションの開発環境を用意するには3つの要素が必要なんだ。

なぜこれらが必要かというと、JSX/Hyperxで書かれたソースをコンパイルするためなんだ💪 コンパイルされると、HyperAppの h関数という仮想DOMを生成するための関数になるのだけれど、それについてはまた後で語るからね。

コンパイル前(JSX/Hyperx)

<h1 id="test">Hi.</h1>

コンパイル後

h("h1", { id: "test" }, "Hi.")

こんな具合だよ。いい具合だね。
さぁ次の章ではJSXとHyperx各々のためのビルド環境のセットアップを学ぶよ。ついておいで!

JSXを使う環境の用意

JSXはXMLと同様データの記法のひとつだよ。知ってたかい? これを使うことでHTML(テンプレート)とJavaScript(処理)を一つのファイルに混在させることができるよ。

JSXを使うには上述の通りコンパイルが必要なんだ。JSXをh関数に変換し、ひとつのjsファイルにバンドルし、配信しなければならないからね。ではさっそくビルド環境の用意をしよう。

Browserifyを使う場合

必要なモジュールのインストール
$ npm install -S hyperapp

$ npm install -D babel-plugin-transform-react-jsx babel-preset-es2015 babelify browserify bundle-collapser uglifyify uglifyjs
.babelrcの用意
{
  "presets": ["es2015"],
    "plugins": [
      [
        "transform-react-jsx",
        {
          "pragma": "h"
        }
      ]
    ]
}
ビルドの実行
$(npm bin)/browserify \
  -t babelify \
  -g uglifyify \
  -p bundle-collapser/plugin index.js | uglifyjs > bundle.js

Webpackを使う場合

必要なモジュールのインストール
$ npm install -S hyperapp

$ npm install -D webpack babel-core babel-loader babel-preset-es2015 babel-plugin-transform-react-jsx
.babelrcの用意
{
  "presets": ["es2015"],
    "plugins": [
      [
        "transform-react-jsx",
        {
          "pragma": "h"
        }
      ]
    ]
}
webpack.config.jsの用意
module.exports = {
  entry: "./index.js",
  output: {
    filename: "bundle.js",
  },
  module: {
    loaders: [{
      test: /\.js$/,
      exclude: /node_modules/,
      loader: "babel-loader"
    }]
  }
}
ビルドの実行
$(npm bin)/webpack -p

Rollupを使う場合

必要なモジュールのインストール
$ npm install -S hyperapp

$ npm install -D rollup rollup-plugin-babel rollup-plugin-node-resolve rollup-plugin-uglify babel-preset-es2015-rollup babel-plugin-transform-react-jsx
rollup.config.jsの用意
import babel from "rollup-plugin-babel"
import resolve from "rollup-plugin-node-resolve"
import uglify from "rollup-plugin-uglify"

export default {
  plugins: [
    babel({
      babelrc: false,
      presets: ["es2015-rollup"],
      plugins: [
        ["transform-react-jsx", { pragma: "h" }]
      ]
    }),
    resolve({
      jsnext: true
    }),
    uglify()
  ]
}
ビルドの実行
$(npm bin)/rollup -cf iife -i index.js -o bundle.js

Hyperxを使う環境の用意

JSXのときとインストールするファイルが少々変わるだけなので割愛するよ😌

API Reference

ここではHyperAppのモジュールや関数の使い方について、サンプルと併せて紹介するよ。

HyperAppは大きく分けて3つの仕組みを提供しているんだ。

  • h 関数 … 仮想DOMを生成する関数
  • app 関数 … HyperAppを利用したApplicationを実行する関数
  • Router プラグイン … ルータ機能群

h

仮想DOMを返す関数だよ。ここで言う仮想DOMとは、ネストされたDOMのツリーをJavaScriptのオブジェクトとして表現しているものだよ🌴

Syntax

h(tag, data, children);
  • tag {String} … 「div」など、HTML上でのタグ名
  • data {Object} … Elementに挿入されるattributes
  • children {String | Array} … 子要素
h('a', {href: '#'}, 'next page');

// return object
// {
//   tag: 'a',
//   data: {
//     href: '#'
//   },
//   children: 'next page'
// }

app

HyperAppによるWebアプリケーションを起動するよ。これを呼び出すことで全てが始まる。オプションを添えられるよ。

app({
  model,
  view,
  actions,
  subscriptions,
  plugins,
  root
});

model

アプリケーションのStateを管理するオブジェクトだよ。primitiveな値でも、配列でも、Objectでもいいよ。

view

仮想DOMを返す関数だよ。引数にmodelactionsをとるよ。

app({
  model: true,
  actions: {
    toggle: model => !model,
  },
  view: (model, actions) =>
    <button onClick={actions.toggle}>
      {model.toString()}
    </button>
});

actions

Webアプリケーションの振る舞いを定義する関数のコレクションだよ。actionsは一般的にmodelを更新するために利用されるよ。そのため、返り値が新しいmodelであることがしばしば。

引数は4つだよ。

  • model … 現在のmodel
  • data … actionの処理(モデルの更新)に必要な情報
  • actions … アプリケーションが持つ大元のactions
  • error … エラー時に呼ばれるコールバック
app({
  model: 0,
  actions: {
    add: model => model + 1,
    sub: model => model - 1,
  },
  view: (model, actions) =>
    <div>
      <button onClick={actions.add}>
        +
      </button>
      <h1>{model}</h1>
      <button onClick={actions.sub}
        disabled={model <= 0}>
        -
      </button>
    </div>
});

subscriptions

DOMContentLoaded時に一度だけ発火される関数をもつ配列だよ。

app({
  model: { x: 0, y: 0 },
  actions: {
    move: (_, { x, y }) => ({ x, y })
  },
  subscriptions: [
    (_, actions) =>
      addEventListener("mousemove",
        e => actions.move({
          x: e.clientX,
          y: e.clientY,
        })
      )
  ],
  view: model => <h1>{model.x + ", " + model.y}</h1>
});

Router

viewに、PathをKeyにTemplateを登録するとルーティングが行えるよ。SPAになるよ😘

app({
  view: {
    "/": (model, actions) =>
      <div>
        <h1>Home</h1>
        <button
          onclick={_ => actions.router.go("/about")}>
          About
        </button>
      </div>,

    "/about": (model, actions) =>
      <div>
        <h1>About</h1>
        <button
          onclick={_ => actions.router.go("/")}>
          Home
        </button>
      </div>
  },
  plugins: [Router]
});

Lifecycle Methods

仮想DOMのライフサイクルにまつわるイベントをハンドリングできるよ。

  • onCreate … ElementがDOMとして構築されたとき
  • onUpdate … Elementの要素が更新されたとき
  • onRemove … ElementがDOMから消える直前
const node = document.createElement("div")
const editor = CodeMirror(node)

const Editor = options => {
  const setOptions = options =>
    Object.keys(options).forEach(key => editor.setOption(key, options[key]))

  const onCreate = elm => {
    setOptions(options)
    elm.appendChild(node)
  }

  const onUpdate = _ => setOptions(options)

  return <div onCreate={onCreate} onUpdate={onUpdate}></div>
}

Afterword

良さそうなところ

  • 小さな開発用ツールなどをつくるときには高速で実装できるし、実行速度的にも◎
  • 特にReactでJSX使ってた人はとっつきやすいかも
  • 小さなライブラリなので、初めてのOSSコードリーディングにはもってこい
  • 同じく、初めてのOSSコントリビュートにはもってこい(?)

ちょっと考えものなところ

  • テンプレートファイルを外出しできないとマークアップエンジニアとの分業がしにくいかも
  • もう少し細かい単位でライフサイクルのイベントハンドリングをしたくなるかも
  • viewの共通部品の定義(Abstract的なもの)とか欲しくなりそう
  • SSR対応できる?

一部訳すのをさぼりました。すみません😵

初めて翻訳(の真似事)をしたのですが、自然と海外ドキュメンタリーの吹き替えみたいな声が脳内再生されました。「私ゃ失敗こいちまってさ」的なやつです。ともあれ、とても楽しかったですしHyperAppを好きになれて満足しました。

間違いや指摘箇所があれば@aloerina_までご連絡ください。

  1. 執筆時点(2017/03/17)でのREADME, wikiを参考にしています。 

  2. 英語が得意でない者が雰囲気で訳したものです。本家HyperAppとは無関係の個人的な記事です。