Generatorと非同期処理
今日はes6の仕様であるGenerator
についてです。
es6でカスタムイテレータを作る (Iterator, Iterable, Generator) - embryo
で簡単に紹介しましたが、今回はGeneratorで非同期処理を扱ってみたいと思います。
co で非同期処理
co
とはGeneratorを利用して非同期処理を同期的に記述することを目的としたライブラリです。
下記のように一連の非同期処理を同期的に実行できるため、処理をシンプルに記述できる他、同一スコープ内で非同期処理の結果を共有することも可能になります。
function asyncTask(value) { return new Promise(resolve => { setTimeout(() => { resolve(value); }, 1000) }); } co(function *() { let result = yield asyncTask(100) console.log(result); })
今回はこの実装を参考にしつつ、簡易な方法で実装していきます。
普通にGeneratorを使う
まずGeneratorを素直に使用する方法です。Generatorはnextを持っているため、このnextを非同期処理の完了時に呼び出すことで 非同期処理を順次実行することができそうです。
この場合Generatorを処理の外で実行する必要がある他、Generatorを仕様側が知っている必要があります。そこで次の実装ではGeneratorを意識せずとも実行できるように書いてみます。
Generatorを内部で実行する
Generatorを内部で実行する方法です。Generatorの引数にcallbackを引数として渡しており、実行側では任意のタイミングでcallbackを実行すればよいため、generatorを意識せずに使用できます。しかしこの場合仕様側はコールバックを順次呼びだす必要があるため、本質的には非同期処理的な書き方と言えます。
Promiseを使用する
iteratorのresult.doneがtrueを返すまで、PromiseのonFullfilledが呼ばれる度にgenerator#nextを再帰的に呼び出します。仕様側はGeneratorやコールバックを意識すること無く、かつ同期的に非同期処理を実行することが出来るようになりました。
前述したco
も仕組みとしては大体同じです。(coではエラーハンドリングなども考慮されていますが今回は説明のため簡易の実装にとどめています)
おまけ
ちなみに現在仕様策定中の async
, await
を用いれば co
のようなライブラリに頼らずとも標準で上記のような処理を書くことが出来ます。
https://tc39.github.io/ecmascript-asyncawait/
async function () { console.log(await new Promise().resolve(100)); console.log(await new Promise().resolve(200)); console.log(“complete”); } /* 100 200 complete */
参考
es6でカスタムイテレータを作る (Iterator, Iterable, Generator)
今日はes6の仕様であるIterator
、Iterable
についてです。仕様を調べつつ実装して挙動を確かめていきます。
Iterator
一連の処理中において現在の処理位置を把握しつつ、コレクション中の項目へ一つずつアクセスする方法を指します。es6においては下記のnext
メソッドを実装したものを指します。
※実際に下記のようなIteratorクラスが定義されているわけではありません。
class Iterator { next(): { done: boolean, value: any } }
例えば以下のような実装はiteratorになります。
let iterator = {} iterator.next = () => { return { done: false, value: 1 } } console.log(iterator.next().value) // 1
であり、done
がtrue
で返ってくる場合、value
は省略されます。
Iterable
iteratorプロトコルとは別にiterableプロトコル
というプロトコルが定義されています。iterableプロトコルを実装したオブジェクトはfor … of
文が使用可能になります。
for … of 文はパラメータを列挙するオペレータで以下のように使用できます。
let iterable = [1, 2, 3] for (item of iterable) { console.log(item) } // 1 // 2 // 3
具体的には下記の[Symbol.Iterator]
メソッドを実装したものを指します。また返却値は前述したIterator
プロトコルになります。
class Iterable { [Symbol.Iterator](): Iterator }
例えば以下のような実装はIterable
になります。
let iterable = {} iterable[Symbol.iterator] => { return next() => { return { done: false, value: 1 } } }
Linked List構造のIterableを作る
Iterableプロトコルを実装することで、ビルトインのiterable以外のものを自前で実装可能であることがわかりました。
そこでLinkedList
の構造を持ったIterableオブジェクトを作って試していきます。LinkedListの実装は下記に公開しています。
class IterableLinkedList extends LinkedList { [Symbol.iterator]() { let node = this.head; return { next: () => { if (!node) { return { done: true }; } let value = node.value; node = node.next; return { value: value, done: false }; } }; } }
自身でカスタムイテレータを作る場合、やはり内部のコードは煩雑になってしまいますね。またIterable, Iteratorプロトコルを正しく理解していなければなりません。そこで次は同じくes6の仕様であるGeneratorを使って同じものを実装してみます。
Generatorを使用した実装
Generator関数はIteratorプロトコルに準拠しているため、Generator関数を用いることでよりシンプルにIterableを実装することが可能になります。メソッドに定義する場合は関数名の先頭に*
をつけることでGenerator関数が私用可能になります。
class IterableLinkedList extends LinkedList { * [Symbol.iterator]() { let node = this.head; while (node) { yield node.value; node = node.next; } } }
Generatorを使用することでnextメソッドの定義や、doneの制御などが不要になり、シンプルに記述することが出来ました。
参考
React+Typescriptでコンポーネント公開用のBoilerplateを作った
今日はReact
製のコンポーネントを外部公開するための雛形を作成したのでその紹介です。
ソースは以下。
構成
- AltJS: Typescript
- Development・Docs: Storybook
- Unit test: Jest
Typescript
型が欲しいのでTypescript
を使用しています。普段も大体Typescript
で書いています。
Jest
単なるコンポーネントのテストが目的なのでシンプルなパッケージ構成で簡単にテストできる jest
を使用しています。
Typescriptを設定する
テストも型安全に書きたいのでts-loader
を使用しています。package.json
に以下の設定を追記します。
"jest": { "moduleFileExtensions": [ "ts", "tsx", "js" ], "transform": { "\\.(ts|tsx)$": "<rootDir>/node_modules/ts-jest/preprocessor.js" }, "testRegex": "/__spec__/.*\\.(ts|tsx|js)$" }
レンダリング結果をテスト
Componentのレンダリング結果をテストするためにairbnb製の enzyme
を使用しています
React.TestUtils
でも問題ありませんがjest
公式で説明されているためこちらを使用します。
GitHub - airbnb/enzyme: JavaScript Testing utilities for React
import Component from "../Component"; import * as React from "react"; import * as Enzyme from "enzyme"; describe("Component", () => { it("content", () => { // Render a component const component = Enzyme.shallow(<Component />); expect(component.text()).toEqual("Awesome component"); }); });
Storybook
開発中の動作確認の他、StyleGuide生成のために storybook
を使用しています。
他にも style-guidist
, catalog
など選定に上がりましたが、フレームワークとしてのルールやメンテの容易さから storybook
を選定しました。
Typescriptを設定する
公式の説明通りにセットアップを済ませると、.storybook以下にファイルが生成されるのでカスタマイズしたい場合はファイルを編集します。
.storybook ├── config.js └── webpack.config.js
私はes6で設定を記述したいのでwebpack.config.babel.js
を作成し、webpack.config.js
から読み込んでいます。実際の設定は以下のようになります。今回はts-loader
を追加し、Typescriptを使用できるようにしています。
import path from "path"; import genDefaultConfig from "@storybook/react/dist/server/config/defaults/webpack.config.js"; module.exports = function(baseConfig, env) { const config = genDefaultConfig(baseConfig, env); config.resolve.extensions.push(".ts", ".tsx"); config.module.rules.push({ test: /\.(ts|tsx)$/, include: [/stories/, /src/], loader: require.resolve("ts-loader") }); return config; };
Storyファイルを書く
stories配下にファイルを配置して、実際のデモを記述します。
import * as React from "react"; import { storiesOf } from "@storybook/react"; import { Component } from "../src/Component"; storiesOf("Components", module) .add("Awesome component", () => <Component />)
Storyファイルを読み込む
.storybook/config.jsを編集してファイルを追加します。UIや設定をカスタマイズしたい場合はオプションを設定します。
import { configure } from "@storybook/react"; const loadStories = () => { require("../stories/index.tsx"); } configure(loadStories, module);
あとはnpm scriptにstorybook
が追加されているので実行してブラウザで確認します。
参考
UniversalLinksの挙動について
OpenIDConnectのクライアント実装のためにUniversalLinksを使用したので、その挙動についてまとめました。
Webブラウザからアプリへの遷移方法
AppScheme
app://[*]
でアプリへのリンクを実現しようというもの。被ったり敢えて被せてくるセキュリティリスクがあるため、iOS9以降では非推奨になっています。
UniversalLinks
アプリ起動のためのよりセキュアな仕様。詳しくは下記参照。
App Search Programming Guide: Support Universal Links
webサイトのURLとアプリケーションを紐付けるため名前被りの心配が不要になる他、関連付けをwebサーバーに問い合わせる必要があるためよりセキュアになります。
実装
1. apple-app-site-association
をwebサーバーに設置する
appID
にはTeamID
とBundle Identifier
をつなぎ合わせたものを指定します。paths
には設定したいパスを設定します。特に指定しない場合は*
を指定します。
"applinks": { "apps": [], "details": [ { "appID": <TeamID>.<Bundle Identifier>, "paths": ["/path/to/website"] } ] }
https配信に対応している場合は上記のファイルを設置するだけで完了です。(2の手順は必要ありません)
2. SSL証明書で署名する
1.で作成したファイルをhandoff.json
として保存し、キーチェーンアクセスから、iPhone Distribution 用の SSL証明書を書き出します(certificate.p12)。
証明書から秘密鍵を書き出します。
openssl pkcs12 -in certificate.p12 -out certificate.key -nocerts
証明書から公開鍵を書き出します。
openssl pkcs12 -in certificate.p12 -out certificate.pem -nodes -clcerts -nokeys
handoff.json を署名します。
cat handoff.json | openssl smime -sign -inkey certificate.key -signer certificate.pem -certfile certificate.pem -noattr -nodetach -outform DER > apple-app-site-association
ここで出力したファイルをwebサーバーに設置すれば完了です。
3. アプリ側の設定
CapabilitiesでAssociated Domainsを有効(ON)にし、以下の値で設定します。
applinks:準備したwebサーバーのドメイン
4. アプリ側の処理
application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([Any]?) -> Void) -> Bool
を用いてアプリケーション側のハンドリングを追加します。 SearchAPI や Handoff にも反応してしまうため、activityTypeでフィルタリングする必要があります。
func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([Any]?) -> Void) -> Bool { // Universal linksを処理します if userActivity.activityType == NSUserActivityTypeBrowsingWeb { // handle universal links } return true }
OpenIDConnectを実現する上での注意点
基本的にユーザー操作に起因しての遷移以外ではUniversalLinksによるアプリ起動は行われない仕様になっているため、状況に応じて以下のような対応が必要になります。(※iOS-Safariのアップデートによって挙動が変わる可能性があります。)
リソースプロバイダ側で認可前の場合
- リソースプロバイダ側で入力必須であり、ログインボタンを明示的に押下する必要があるので問題なし
redirect_url
先でUniversalLinksのURLにリダイレクトしても問題なし
リソースプロバイダ側で認可済みの場合
- リソースプロバイダ側で「アプリに戻る」ボタンを設置
- もしくはUniversalLinks対象の画面に「アプリに戻る」ボタンを設置
ちなみにfacebookなども認可済みの場合は「アプリに戻る」ボタンを設置しています。
その他
apple-app-site-association の設置状況や内容が正しい状態になっているかの確認は下記チェッカーを使用しました。うまくいかない場合はこちらで確認してみると便利です。
参考
FluxとDDD(レイヤードアーキテクチャ)について考えてみた
トレタ Advent Calendar 2016 - Qiita 16日目の記事になります。 フロントエンドエンジニアのすえだです。
はじめに
この記事で書いていること
- Fluxについて
- DDD(レイヤードアーキテクチャ)について
- FluxとDDDの関係性
- Fluxのレイヤー化
Fluxについて
単方向に伝搬されるデータでアプリケーションの状態を表現するアーキテクチャパターンです。 CQRSとEvent Sourcingを組み合わせたような形になります。
恩恵
- 単方向のデータフローにより、行ったり来たりのような処理が少なく振る舞いを理解しやすい
- DispatcherやActionがシングルトンで表現されるので参照に悩まされづらい
DDD(レイヤードアーキテクチャ)について
DDD自体はドメインモデルを中心に置いたサービス設計手法とのこと。 実装においてはドメインモデルと技術的関心を分離するために 上記のようなレイヤードアーキテクチャが推奨されています。 (Clean Architectureなどいくつか発展型や派生系がありますがここでは基礎となるこれだけを紹介します)
Infra
定義の上では「上位のレイヤーを支える技術的な基盤」とあり、ライブラリやフレームワークなんかもここに含まれるのですが 本記事では簡単のためにDBやAPIなどの永続化を担う層として考えます。
Domain
いわゆるビジネスロジックになりますが 噛み砕いて言うと「あるデータとその振る舞い」を表現する層になります。
Application
アプリケーションに依存する振る舞いです。 ドメイン層のオブジェクトを協調させるような処理を担当します。
Presentation
UIなどユーザーにとってどう見せたいかを実現する層です。
恩恵
- 責務が分離されているので実装時に迷うことがない
- レイヤーが分離されているのでフレームワークに依存せず、リプレイスも容易
FluxとDDD
Fluxの各レイヤーをDDDの視点で整理していきます。
View
Presentation層にあたります。理解しやすいので説明を割愛します。
Action
その名の通り「振る舞い」を記述する部分になります。 APIと疎通し、取得したデータをStoreに伝搬するなど、ある振る舞いに依存するドメインモデルの協調動作を取るような働きをしています。このことからActionはアプリケーションロジックを担っていると言えそうです。
Store
Actionから受け取ったイベントをもとにアプリケーションの状態を変更し、Viewに状態の変更を伝えます。 名前から永続化を担当するInfra層かのように見えますがAPIとの疎通は基本的にActionが行うため、永続化の責務はAction(もしくはActionから実行されるInterface)が担っているように見えます。
しかしながらActionはオンメモリのドメインオブジェクトの集合への参照はもっていないため、バリデーションなどのロジックはStoreが受け持つことになり、ドメインのロジックを含んでいると言えます。
また実装によってはDBへの書き込みはStoreで行ったり、Viewからの参照があるためにViewModelのような属性を持ったりすることもありえるでしょう。
このようにStoreの責務が定まっていないことがFlux実装の曖昧性の原因ではないでしょうか。
Fluxのレイヤー化
そこでStoreの責務に焦点をあてて構造化を目指します。
方針
- Flux実装における Storeの責務が曖昧なので明確にしたい、また必要であれば分割したい
- Fluxの良さをできるだけ損なわずにDDDの思想を取り入れたい
- Storeの破棄についてのケア
3つ目は本記事の内容とズレますがここをカバーしていないと夢物語になりそうなので入れておきます。
こんな感じになった
名前は適当です。
Repository
Repositoryパターンの解釈が分かれるところかとは思いますが、Fluxが解決する問題は基本的にオンメモリの上のやり取りなのでここでいうRepositoryパターンで扱うのは APIまたはDBなどとの処理になります。「APIからデータを取得し、DBに保存する」などの責務を追うことになります。
DomainStore
ドメインオブジェクトに対応したデータを格納するオンメモリのデータストアとして機能します。依然としてドメインロジックは持つことになるのですが、以前の状態よりは責務がスッキリしたかと思います。
PresenterStore
ある興味領域の状態を管理します。またDomainStoreの状態を監視しており、その状態変更に応じて状態を変更します。
またPresenterStoreはあるプレゼンテーション層に紐付いたものなので、プレゼンテーション層のライフサイクルにおいて破棄が可能です。 そのため、DomainStoreをシングルトンにすべきかは定めていません。DomainStoreがPresenterStoreによって管理されている場合は PresenterStoreの責任でDomainStoreに保存したデータも破棄することが可能です。
まとめ
- FluxをDDDの視点で見たときの各レイヤーの関係性を整理した
- FluxのStoreを構造化してみた
最後に
Flux実装やVirtualDOMの登場でフロントエンド人口は増えてきて嬉しいのですが、ちゃんとアーキテクチャについて議論できる場は減ったように感じます。(フレームワークやライブラリの話になりがち) なので、このタイミングで今までのアーキテクチャの変遷を整理するのは次なる発展のためにも必要なことだと思いました。
またここでは詳しく話しませんでしたが、他のレイヤードアーキテクチャやCQRS、EventSourcingなんかについてもいつか紹介したいと思います。
明日はmasuidriveによる「React + Rails」の話です。お楽しみに!
クローゼットがいっぱいになったので壁にハンガー掛けを作ってみた – @masuidrive blog
DIYについての話でした(・∀・)
参考資料
DDD, CQRS, EventSourcing
- 実践 Domain-Driven Design : アプリケーション層のクラス | システム設計日記
- イベント・ソーシングを知る
- Domain-Driven Designのエッセンス
- Agile, Architecture, DDD and CQRS
Flux and DDD
- DDD + Flux? // Speaker Deck
- What the Flux? (On Flux, DDD, and CQRS) — Jack Hsu
- 複雑なJavaScriptアプリケーションを考えながら作る話