こんにちは、@binarytaです。
最近ちょくちょく噂を聞くHyperappに興味を持ったのでTypescriptと組み合わせて実践してみようと思います。
Vue.jsやReactもよく触れているのでこの辺との違いについても言及できたらと思ってます。
まずHyperappが提唱するコンセプトがこちら。
- Minimal — Hyperapp was born out of the attempt to do more with less. We have aggressively minimized the concepts you need to understand while remaining on par with what other frameworks can do.
- Functional — Hyperapp's design is inspired by The Elm Architecture. Create scalable browser-based applications using a functional paradigm. The twist is you don't have to learn a new language.
- Batteries-included — Out of the box, Hyperapp combines state management with a Virtual DOM engine that supports keyed updates & lifecycle events — all with no dependencies.
超軽量で関数型、状態更新等の効率化を謳っています。
Hyperapp自体のソースコードはそれほど多くなく、機能も最小限に抑えていそうなのであまり複雑な概念とかはなさそうです。
ということで早速動かしてみます。
環境構築
今回はwebpackでビルドしていくので、package.jsonとwebpack.config.jsの内容を貼っておきます。
package.json
{ "name": "hyperapp_typescript_introduction", "version": "1.0.0", "main": "index.js", "license": "MIT", "devDependencies": { "awesome-typescript-loader": "^3.4.1", "babel": "^6.23.0", "babel-core": "^6.26.0", "babel-loader": "^7.1.2", "babel-plugin-transform-react-jsx": "^6.24.1", "babel-preset-env": "^1.6.1", "babel-preset-es2015": "^6.24.1", "ts-loader": "^3.5.0", "tslint": "^5.9.1", "tslint-loader": "^3.5.3", "typescript": "^2.7.2", "webpack": "^3.11.0" }, "scripts": { "tsc": "tsc", "build:watch": "webpack -w --config webpack.config.js" }, "dependencies": { "hyperapp": "^1.1.2" } }
webpack.config.js
const path = require('path'); module.exports = { entry: [path.resolve(__dirname, './src/app.ts')], output: { filename: 'bundle.js', path: path.join(__dirname, 'public/') }, module: { rules: [ { test: /\.tsx?$/, exclude: /node_modules/, enforce: 'pre', loader: 'tslint-loader', options: { failOnHint: true } }, { test: /\.tsx?$/, exclude: /node_modules/, loader: 'ts-loader' } ] }, resolve: { extensions: ['.ts', '.tsx'] } }
現状で全て最新のものを使用してます。
完成後のディレクトリ構成はこんな感じ。
. ├── package.json ├── public │ ├── bundle.js │ └── index.html ├── src │ ├── actions.ts │ ├── app.ts │ ├── state.ts │ └── view.tsx ├── tsconfig.json ├── webpack.config.js └── yarn.lock
htmlの中身はこんな感じです。
public/index.html
<!DOCTYPE html> <html> <head> <meta charset="utf-8"/> <title>hyperapp + typescript sample</title> </head> <body> <script src="bundle.js"></script> </body> </html>
実践
Hyperapp公式リポジトリに載っているサンプルをTypescriptで実装してみることにします。
Hyperappの主な概念は、View, Action, Stateの3種類です。
- View: DOMの記述(JSXかh関数)
- Action: 状態を更新するための関数.
- State: プレーンなObject. Actionから状態を更新する.
ViewはStateとActionを受け取り、Viewの中で各Nodeに適宜StateとActionを割り当てていくだけです。
今回は以下のようにファイルを複数に分割して書いていきます。
src/ ├── actions.ts # Actions定義ファイル ├── app.ts ├── state.ts # State定義ファイル └── view.tsx # View定義ファイル
ではまずStateから実装していきます。
Stateの実装
Typescriptで書くためObjectに対する型定義としてinterfaceを実装します。
そしてstateオブジェクトはそのinterfaceを型として実装します。
state.ts
export interface State { count: number; } export const state: State = { count: 0 }
Actionsの実装
actionsオブジェクトはActionsType
という型で定義します。
ActionsTypeはジェネリクス引数に自分で実装する2つの型、 State
とActions
を期待します。
各Action (up, down) の戻り値はActionResult<State>
という形で型定義ファイルに実装されているので、その通り戻り値に指定します。
なのでまずこちらも同様にinterfaceでActionsの型を定義し、actionsオブジェクトはActionsTypeにState
とActions
をジェネリクス
引数に与えた型として定義します。
actions.ts
import { ActionsType, ActionResult } from "hyperapp" import { State } from "./state" export interface Actions { down: (value: number) => (state: State) => ActionResult<State>; up: (value: number) => (state: State) => ActionResult<State>; } export const actions: ActionsType<State, Actions> = { down: (value: number) => (state) => { return { count: state.count - value } }, up: (value: number) => (state) => { return { count: state.count + value } } }
Viewの定義
HyperappではReactのようなJSX構文を用いてViewを実装できます。
別の方法としてHyperscriptのh関数
記法でも実装できるみたいです。
JSXの方が楽なので、JSXで実装していきます。
単にJSXでviewを実装するだけなのであまり説明がいらないですね。
しっかり型チェックも効いていて、
<button onclick={() => actions.down("1")}>-</button> <button onclick={() => actions.up("1")}>+</button>
のように、number
型を期待しているactionにnumber
型以外を与えてしまうとエラーを吐いてくれます。
Vue.jsだとSingleFieComponent(単一ファイルコンポーネント) のtemplate部分はTypescriptを使っていても型チェックがされないので、JSXはその点すごくいいとおもいます。
view.tsx
import { h, app, View } from "hyperapp" import { state, State } from "./state" import { actions, Actions } from "./actions" export const view: View<State, Actions> = (state, actions) => ( <div> <h1>{state.count}</h1> <button onclick={() => actions.down(1)}>-</button> <button onclick={() => actions.up(1)}>+</button> </div> );
State, Actions, ViewをDOMに割り当てる
仮想DOMを実際のDOMに割り当てるための最後の実装です。
今回実装したState
, Actions
, View
をapp関数に引数として渡して、どのノードに割り当てるかという情報を与えてあげるだけです。
import { h, app } from "hyperapp" import { state, State } from "./state" import { actions, Actions } from "./actions" import { view } from "./view.tsx" app<State, Actions>(state, actions, view, document.body);
Hyperappの型定義ファイルについて
VueやReact, Angularなど有名なフレームワークは型定義ファイルもそれなりに膨らんでいて、Typescriptで型を明示するときにそのフレームワークの型定義ファイルを読み解くのに時間がかかったりすることがあります。
型定義ファイルがシンプルに保たれているのは魅力のひとつです。
ちょっとだけ内部の型定義ファイルを覗いて見ます。
hyperapp.d.ts
/* ~~~~~~~~~~ */ export type ActionsType<State, Actions> = { [P in keyof Actions]: | ActionType<State, Actions> | ActionsType<any, Actions[P]> } /* ~~~~~~~~~~ */ export interface View<State, Actions> { (state: State, actions: Actions): VNode<object> } /* ~~~~~~~~~~ */ export function app<State, Actions>( state: State, actions: ActionsType<State, Actions>, view: View<State, Actions>, container: Element | null ): Actions /* ~~~~~~~~~~ */
Stateという型パラメータは内部で定義されていないので自身でinterface
またはclass
で型を実装してあげる必要があります。
次にactionオブジェクトはActionsType
という型で定義していました。
これはapp関数の第2引数のactionsがActionsType<State, Actions>
を期待してるためです。
またActionsという型パラメータも自分で実装してあげる必要があります。
最後にviewオブジェクトは<State, Actions>
をジェネリクス引数で受け取りJSXを実装する関数でした。
JSX (またはh関数) は最終的にVNode<object>
型となるためです。
パフォーマンス比較
Hyperappが他の2つより最も優れているのはstartup time
(読み込み、解析、起動) でした。
逆に他の2つに比べてパフォーマンスが劣化しているのは更新系処理の partial update
, replace all rows
です。
軽量なだけあって、VueやReactに比べて操作に必要なメモリが少なくて済んでいます。
まとめ
Reactを使用したときにPropsの型定義などで時間を費やしたこともあったので、やはり型定義ファイルがシンプルなのは魅力ですね。
一方Vue.jsはvue-class-component
というライブラリを使用すると型をアノテーションで注釈してあげるだけでいいので楽です。
Hyperappは他のJavaScriptフレームワークと比較すると非常に学習コストも低く習得しやすいです。また、多くのJavaScriptフレームワークのコンポーネント思考や仮想DOMの概念なども取り込んでいるので他のフレームワークに移行する際の障壁もかなり低くなるはずです。
Hyperappはちらほら耳にするようになったので今後が期待です!