JavaScript
TypeScript
hyperapp

フロントエンド初心者がHyperappに触れてみる

hyperappを使って公式に載っているごく簡単なカウンターアプリを作りました。

image.png

開発環境の構築

フロントエンドわからないマン(reactの本と公式チュートリアル1周した程度)なのでとにかく楽に導入できる方法を選択しました。

# 作業フォルダの設定
PS> mkdir try-hyperapp
PS> cd try-hyperapp

# package.jsonの作成
PS> yarn init -y

# Hyperappのインストール
# babel-preset-envのインストール(環境に合わせてトランスパイルしてくれる?)
# parcelのインストール(設定ファイルのいらないモジュールバンドラー)
PS> yarn add hyperapp babel-preset-env parcel-bundler

最終的なpackage.jsonは以下のようになりました。

package.json
{
  "name": "try-hyperapp",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "dependencies": {
    "babel-preset-env": "^1.6.1",
    "hyperapp": "^1.2.5",
    "parcel-bundler": "^1.8.1"
  },
  ※↓TypeScriptで書いたのでparcelでバンドルした際に自動で追加されたようです。
  "devDependencies": {
    "typescript": "^2.8.3"
  }
}

TypeScriptで書きたいのでtsconfig.jsonを作成します。

tsconfig.json
{
  "compilerOptions": {
    "jsx": "react",
    "jsxFactory": "h",
    "sourceMap": true,
    "strict": true
  }
}

counter.tsxの作成

まず、ルートディレクトリ直下にsrcフォルダを作成します。
本当ならactionsフォルダやviewsフォルダに分けたほうが良いのかもしれませんが、今回は簡単にsrc直下に全てまとめたcounter.tsxを作成します。

src/counter.tsx
import { h, app, ActionsType, View } from "hyperapp";

interface IState {
  count: number;
}
const state: IState = {
  count: 0
};

interface IActions {
  down: (value: number) => IState;
  up: (value: number) => IState;
}
const actions: ActionsType<IState, IActions> = {
  down: value => state => ({ count: state.count - value }),
  up: value => state => ({ count: state.count + value })
};

const view: View<IState, IActions> = (state, actions) => (
  <div class="card center-align">
    <div class="card-image">
      <h1>{state.count}</h1>
    </div>
    <div class="card-action">
      <button
        class="waves-effect waves-light btn-large"
        onclick={() => actions.down(1)}
      >
        sub
      </button>
      <button
        class="waves-effect waves-light btn-large"
        onclick={() => actions.up(1)}
      >
        add
      </button>
    </div>
  </div>
);

app<IState, IActions>(state, actions, view, document.body);

ActionsTypeViewはTypeScriptでもインポートしなくてもコンパイラに怒られませんが、型が定まっているほうがうっかりミスが減ったり補完が効くので導入しました。

actionsvalueany型になってしまい、若干不安でしたが、
image.png

使用する際にはインターフェースの通りnumber型と推論されていたので安心しました。
image.png

Index.htmlの作成

parcelのエントリーポイントになるIndex.htmlをルートディレクトリ直下に作成します。
見た目がさみしいのでCDN経由でMaterialize.cssを使います。

index.html
<!DOCTYPE html>
<html lang='ja'>

<head>
  <meta charset='UTF-8'>
  <title>try hyperapp</title>
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0-beta/css/materialize.min.css">
</head>

<body>
  <script src='./src/counter.tsx'></script>
</body>

</html>

実行

以下のコマンドでdistフォルダが作られてローカルサーバー立ち上がります。
今回、TypeScriptを使いましたが、インストールされていない場合、自動で行ってくれるようです。

PS> yarn run parcel index.html

感想とわからなかったところ

良かったところ

  • reduxのようなelmアーキテクチャ?が最初から組み込まれているのがreactと比べて好印象。
  • action周りが簡潔なところも好きです。
  • TypeScriptと相性がよさそう。
    • 補完がバリバリ効いてうっかりミスもなくなる!
  • ボイラープレート的なコードが少なく、やりたいことが簡潔に書ける

分からなかったところ

  • フォルダ構成のベストプラクティス
    • ちょっと大きめのアプリを作成する際はreactのDucksパターンみたいのがいいのでしょうか?
src
.
|_ views
| |_ counter.tsx
| |_ foo.tsx
|_ states
| |_ counter.ts
| |_ foo.ts
|_ actions
  |_ counter.ts
  |_ foo.ts
  • actionsstateの型はinterfaceがいいのかtypeがいいのかわからない
    • 困っているので詳しい方いらっしゃれば教えてください:bow_tone1:
// interfaceで書く
interface IActions {
  down: (value: number) => IState;
  up: (value: number) => IState;
}
const actions: ActionsType<IState, IActions> = {
  down: value => state => ({ count: state.count - value }),
  up: value => state => ({ count: state.count + value })
};

// typeで書く
const actions: ActionsType<IState, IActions> = {
  down: value => state => ({ count: state.count - value }),
  up: value => state => ({ count: state.count + value })
};
type Actions = typeof actions;
  • デバッグ方法が分からない:scream:
    • vscodeでステップ実行する方法が分からないのでよく調べなければいけない・・・
    • sourcemap:tureにしたりvscodeにchrome開発者拡張入れたりしたけどダメでした