python-prompt-toolkitをgolangに移植。kubernetesクライアント作った

Github Trendingに載ってから、予想以上に伸びました。ありがとうございます。

python-prompt-toolkitをgolangでも使いたいなと前から思い移植していたのですが、今日go-promptという名前で公開しました。

kube-promptは実際にgo-promptを使って作成したインタラクティブなkubernetesクライアントです。

kube-prompt

python-prompt-toolkit の移植

GitHub - jonathanslenders/python-prompt-toolkit: Library for building powerful interactive command lines in Python

python-prompt-toolkitは簡単にいうとインタラクティブな補完ができるライブラリなのですが、これを使った便利なツールがたくさん登場していて、かなりのstarを集めています。

自分はpython3を当然のようにインストールしているので、Pythonのツールを使うことが面倒には感じないのですが、普段pythonを使っていない人はpythonのインストールから始めないといけないので結構面倒そうです。

python-prompt-toolkitをGolangに移植する人が現れないかなと思っていたのですが、中々登場しませんでした。 ふと思い立って実装を読んでみると、テストコードなしの実装だけでも2万行以上とボリュームがあります。この強力な補完のために裏では結構面倒なことをしていて、実装もそれなりに複雑になっているので、なるほどこれは大変と思い挑戦してみました。

自分はこういう端末制御ゴリゴリのソフトウェアを作ったことなく、コードを読む上で必要な前提知識が色々と欠けていたので、いい勉強になりました。この記事では実装に関して書きませんが、需要がありそうなら記事書くかどこかで話すかしたいと思ってます。

詳しい使い方はREADMEに書いたので興味のある方は使ってみてください。比較的かんたんに使えるようになっているかなと思います。 オプションも色々用意してます。

f:id:nwpct1:20170813221708p:plain

今後追加予定の機能は

  • Windowsサポート
    • mattnさんがPRくれました。ありがとうございます。windows環境手元にないのでまだ試せないのですが、どうしようか
  • vi modeのサポート
  • multi line inputに対応
  • emacs キーバインドが本来と違う挙動をする箇所が見つかったので修正
    • Ctrl+Wの挙動だったのですが、b4b4r04さんが直してPRくれました。ありがとうございます。
  • Fuzzy Searchingとかprompt作る上で便利なutility系の用意

あたりです。

kube-prompt

kubernetesクライアントも用意したので、kubernetes触ってる方はぜひ使ってみてください。 Github Releaseからバイナリ落とすか、Homebrewで入ります。

2週間半のReact Nativeアプリ開発を振り返る

React Native+Expoではじめるスマホアプリ開発 ~JavaScriptによるアプリ構築の実際~

React Native+Expoではじめるスマホアプリ開発 ~JavaScriptによるアプリ構築の実際~

ここ2週間ちょっとReact NativeでiOSアプリ書いてました。 サーバサイドをメインでやってきた自分にとって面白い技術で、今後も趣味で使ってみたいなと思えているのでTipsや所感を残しておきます。

目次

選定理由

3人でとあるアプリを開発しているのですが、自分も含めてサーバエンジニア2人、iOSエンジニア(Webフロントとかもできる)1人の状態でした。 開発期間が短いわりに画面数の多いアプリケーションの開発だったので、JS経験のある自分もネイティブに加われるようReact Nativeを検討。苦労しそうな部分は早い段階で、検証しておきました。

  • カメラ撮影
  • iOSライブラリとReact Nativeのブリッジが問題なく作れるか確認
    • 画像のフィルタ処理(iOSのCIFilter)用ライブラリをReact Nativeから呼び出した
  • 動画再生
  • APIクライアント(fetch apiが使える)
  • Firebase JavaScript SDK(Firebase Authenticationを使用)

開発を始めて強く思ったのは、やっぱりネイティブの知識があって気軽に質問できる人が近くにいないと大変だったなと思います。 幸いiOSに詳しいチームメイトがReact NativeとiOSのブリッジを書いてくれて助かった。

一方で、アプリケーションのほとんどの画面はAPIを叩いてレスポンスを表示するだけだったりサーバに何か文字列を送るだけだったり、意外とJavascriptの範囲で出来ることが多くてサーバサイドばかりやってきた自分もがっつりiOSアプリ開発が手伝えます。

普段あんまりネイティブアプリケーションの実装とかを意識することはなかったんですが、スマホのアプリを立ち上げるとコンポーネントの組み方を考えるようになったり、細かいアニメーションに意識がいくようになりました。

Tips

エラーハンドリング・状態管理

f:id:nwpct1:20170528191247g:plain

React Nativeのコンポーネントや状態管理の設計は、ReactでSPAを作るのと感覚的にかなり似ているのかなと思っています。 今回は次のような手順で管理しました。

コンポーネントの状態を管理するStoreを用意。 このStoreでは、各ComponentIDに対して以下の4つの状態を持つフィールドとエラーメッセージを持つFieldが存在する。

  • BEFORE_LOADING : データをフェッチする前(コンポーネント生成時に、この状態に設定するActionを発火)
  • LOADING : データをフェッチ中 (ぐるぐるのローディングアイコン表示)
  • COMPLETE : 無事にデータを読み込みできている状態
  • ERROR : データのフェッチ中に何かしらのエラーが発生している。コンポーネントはこの場合エラーを表示

コンポーネントは次の手順で表示を変える。

  1. constructorで自分自身を示すIDを生成
  2. 各Componentに上のStoreの情報を伝搬させる
  3. componentWillMount で、自分自身のIDをstoreに登録(Storeでは BEFORE_LOADING にセット)
  4. componentDidMount で、データのフェッチをするActionを発火。自分の状態を LOADING に設定するActionも発火
  5. 200番のレスポンスが帰ってきたら
    • COMPLETE に設定するActionを発火
  6. 何か問題があったときは
    • ERROR に設定するActionを発火
    • エラーメッセージを登録する
    • エラー情報を表示するModalアニメーションを表示するActionを発火
  7. コンポーネントは自分自身の状態が ERROR ならエラーメッセージを表示、 COMPLETE なら受け取ったデータを表示

今回選んだ、エラーハンドリングや状態の管理方法はかなりうまく機能していた気がするので今後も積極的に使っていきたい。

入力欄をキーボードの出現に合わせて動かすアニメーション

書くか悩んだのですが、結構多くのアプリケーションで必要になる処理かなと思ったので載せておきます。 必要になった際は参考にしてください。

f:id:nwpct1:20170528193253g:plain

次のようにkeyboardの入力に応じて高さの変わるコンポーネントを用意

import React, { Component } from "react";
import { Animated, Keyboard } from 'react-native';

const defaultHeight = 48;

export class AvoidableKeyboard extends Component {
    constructor(props, context) {
        super(props, context);

        this.state = {
            height: new Animated.Value(defaultHeight)
        };

        this.keyboardWillShow = this.keyboardWillShow.bind(this);
        this.keyboardWillHide = this.keyboardWillHide.bind(this);
    }

    componentDidMount() {
        Keyboard.addListener("keyboardWillShow", (e) => this.keyboardWillShow(e));
        Keyboard.addListener("keyboardWillHide", (e) => this.keyboardWillHide(e));
    }

    keyboardWillShow(e) {
        Animated.timing(this.state.height, {
            toValue: e.endCoordinates ? e.endCoordinates.height : e.end.height,
            duration: e.duration
        }).start();
    }

    keyboardWillHide(e) {
        Animated.timing(this.state.height, {
            toValue: defaultHeight,
            duration: e.duration
        }).start();
    }

    render() {
        return (
            <Animated.View style={{ flexBasis: this.state.height }}></Animated.View>
        );
    }
}

あとは flexGrowflexBasis プロパティをうまく使って表示します。

(省略)

export class CommentListComponent extends Component {
    render() {
        (中略)
        return (
            <View style={{ flex: 1, backgroundColor: "#FFFFFF" }}>
                <ScrollView style={{ flexGrow: 2 }}>
                    {comments.map((c) => <CommentItemComponent key={c.id} comment={c} />)}
                </ScrollView>
                <View style={{ flexBasis: 50, flexDirection: "row", alignItems: "center", borderBottomColor: "#ddd", borderTopColor: "#ddd", borderBottomWidth: 1, borderTopWidth: 1 }}>
                    <TextInput
                        style={{ height: 50, flexGrow: 1, fontSize: 15, paddingLeft: 10 }}
                        onChangeText={(text) => this.setState({ text })}
                        value={this.state.text} />
                    <TouchableWithoutFeedback onPress={this.postComment} disabled={this.state.text.length === 0}>
                        <View>
                            <Text style={[{ fontSize: 15, fontWeight: "bold", paddingRight: 10 }, postTextStyle]}>投稿する</Text>
                        </View>
                    </TouchableWithoutFeedback>
                </View>
                <KeyboardSpace />
            </View>
        )
    }
}

今回使ったライブラリ

使ったライブラリも載せておきます。 ライブラリ選定の際に参考にどうぞ。

react-native-focus-scroll

ScrollViewの中で今ディスプレイに表示されているアイテムを知る方法がなかったので作成・先ほど公開しました。

Vertical Scrolling Horizontal Scrolling
sample - vertical sample - horizontal

react-navigation

今回はiOSアプリのみの開発だったので、公式の NavigatorIOS を最初つかっていたのですが、navigate時に渡したpropsが変化しても値が更新されないようです(NavigatorIOS optimizations break functional components · Issue #13539 · facebook/react-native · GitHub)。 これだと困るシーンがあったのでreact-navigationを使用しました。react-navigaitonのscreenPropsでstoreの情報とかを伝播させています。

f:id:nwpct1:20170528184130p:plain

見た目もカスタマイズ出来る範囲がNavigatorIOSより多そうです。

react-native-vector-icons

TabBarIOSのItemに画像を入れようとすると、うまくリサイズできなくて困っていました。 このライブラリはTabBarIOSに組み込むためのComponentも提供してくれています。あとSVGからttf生成して追加してあげれば自作のアイコンとかも使うことが出来ます。

f:id:nwpct1:20170528183852p:plain

react-native-camera

カメラで使用。 ここは全部チームメイトがやってくれて、自分は使い勝手がわからないのですが無事動いてくれています。

react-native-video

動画の再生で使用。 解説は特にないのですが、muteやpauseもpropsで指定できて使いやすかったです。

firebase

認証はFireabase Authenticationを使用しました。 JS SDKが問題なく動きます。

Firebaseから取得したJWTは、サーバ側で検証するのですが、SDKJavascriptとかPython用が提供されています。 Go用のSDKはないので、 こちらの説明 に従って自分で検証処理を書く必要があります。 検証用のコードを公開したので、ご活用ください。

Verify the JSON Web Token obtained from Firebase Authentication. But now, Go SDK is released by firebase organization. · GitHub

現在は公式でサポートされたので、そちらをご利用ください。

github.com

react-native-user-defaults

iOS限定ですが、JWTの保管はUser defaultsで行いました。

苦労したこと

スタックトレースについて

苦労した点はいくつかありますが、スタックトレースはReact Nativeを採用するか悩んでいる人の多くが心配しているかもしれません。 実際スタックトレースから情報がなかなか読み取れずデバッグが大変なシーンはいくつかありました

実際に発生したエラーのスタックトレースを3つほど載せておきます。

エラー1 エラー2 エラー3

エラー1は、バンドル後のJSファイル jsBundle.js 内での行数が表示されてしまっているケース。 エラー2とエラー3は、Objective-Cのブリッジ等の部分でエラーが起きてそちらのスタックトレースが表示されてしまうケースです。 ここからちゃんと問題を把握・デバッグできるぐらい理解を深めるのであれば、React Nativeを使う理由が減っていくだろうなぁという思いもあり、トライ&エラーで乗り切りました。ある程度はくじけない気持ちが必要かもしれません。

再現性のないバグ

React Nativeに限らないかもしれませんが、再現性のないバグが比較的多く発生してしまた気がします。 自分の実装がまずかった場合も多いかと思いますが、決して成熟しきった技術ではないので、フレームワークやライブラリのバグを疑う回数も多いです。

再現性の無いバグについては自分も理解できていないのであまり多くは紹介しませんが、文字通り時間が解決してしまったケースもあります。 ほとんど参考にならないかとも思いますが、おかしな挙動に遭遇したときに自分がとりあえずやっていることを残しておきます。

  • git cleanでgitの管理対象外のファイルを消して、ビルドし直す
    • git clean -x -d -f -e node_modules -e .idea
    • 何か変更を加えてエラーが出たとき、戻してもまだビルドできないとかがたまにあります。開発序盤はとりあえずこれで乗り越えました
    • 1週間もしてくるとだいたいどの辺に問題があるのか感覚的にわかってきて、git cleanを使う回数が自然と減ってきた気がします
  • 自分が加えた変更が反映されずReloadしてもずっとエラーを吐き続ける場合 (実機Buildはうまくいくけど、Simulatorで動かない場合もある):
    • iPhoneのSimulatorのCacheを消す。git cleanも実行
    • Websocketサーバを一度止めてから react-native run-ios
    • どこにキャッシュが残っているのか、これでも何故か以前のコードがSimulator上で実行されてしまうときがありましたが、気づいたら直っていてもう再現できないので自分もよくわかっていません

サーバサイドのアプリケーションを開発していると、多くのバグは掘り下げることで突き止めることができる気分になれますが、iOSは自分には知らないことが多すぎてとにかく色々手を動かして条件を変えてうまく動くことを祈ることが多かったです。

開発をやってみて

冒頭でも書きましたが、カメラやフィルター処理といったネイティブの機能をガリガリ使うところ以外は自分でもかなり開発に参加できます。 また普段使っているスマートフォンのネイティブアプリケーションもネイティブの機能は使わずAPIの通信と表示のみで成り立っている画面も多いなと感じました。 スマートフォンの細かいアニメーションにも意識がいくようになり、色々とこれまで感じることのなかった面白さがあったかなとおもいます。

また趣味で使ってみたいですね。 終わり。

Protocol BuffersをJSONに変換する

React Native + GoのProobuf API Serverの構成でアプリケーションを作っているのですが、次の条件でdeserializeができないことがわかってしまった。

decodeIO/protobuf.js の方も一応試してみたけど、うまく動かないので急遽Protobufの使用を諦めてjsonに切り替えることにします。 こちらも詳細は↓にまとめました。あとでGithubの方にもIssue建てようかと思います

protobuf/jsonpbJSONに変換する

開発期間も短いので今からサーバの実装を変えていくのはできればさけたい。 モックサーバとかはすでにProtobufで書いていたので、それをJSONに変換する処理を書いていく方法を検討してみた。

今回使っている型はそんなに多くないので自前でさっと書けないかなとか考えつつ調べてみたら、protobufの中に jsonpb という便利そうなモジュールがあった。 ProtobufのモックAPIJSONAPIに書き換えてみる。

func confirmProfile(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(http.StatusOK)

    p := &pb.ConfirmProfileResponse{
        HasProfile: true,
        Profile: &pb.ProfileResponse{
            Uid: "uid0001",
            Username: "c_bata_",
            IconUrl: "http://example.com/hoge.png",
            WebsiteUrl: "http://example.com",
            CreatedAt: 1000,
            UpdatedAt: 1000,
        },
    }

    m := jsonpb.Marshaler{
        EmitDefaults: true,
        Indent: "    ",
        OrigName: true,
    }
    m.Marshal(w, p)
    return
}

curlで確認。

$ curl localhost:8080 
{
    "has_profile": true,
    "profile": {
        "uid": "uid0001",
        "username": "c_bata_",
        "icon_url": "http://example.com/hoge.png",
        "website_url": "http://example.com",
        "created_at": "1000",
        "updated_at": "1000"
    }
}

実装はreflectを多用することになるので、パフォーマンスには課題があるかもしれませんが簡単にJSONAPIに変換できた。 ただ、数値(int64)で欲しかったものがstringになってしまっていて、整数値として出力してほしい。 もしかするとバグかなと思い、ResponseWriterの代わりに次のLoggerBufferを仕込みつつ原因となっている行を探してみた。

type LoggerBuffer struct {
    bytes.Buffer
}

func (b *LoggerBuffer) Write(p []byte) (n int, err error) {
    log.Printf("LoggerBuffer: %s\n", p)
    return b.Buffer.Write(p)
}

次の行が原因らしく、どうやらint64とuint64の型の場合はあえて、ダブルクオーテーションで囲っている。 こうしている理由は、JavaScriptの仕様に何か関係しているのかもしれないと思い調べてみた。

https://github.com/golang/protobuf/blob/master/jsonpb/jsonpb.go#L504-L511

EcmaScriptのNumber型

調べてみると次のStackoverflowの回答が見つかった。

math - What is JavaScript's highest integer value that a Number can go to without losing precision? - Stack Overflow

Number型で表現可能なのは +/- 253 の範囲らしい。 あとbit演算は32bitの範囲で利用できる。 実際にChromeのConsoleで挙動をみてみた。

> Math.pow(2, 53)
9007199254740992
> Math.pow(2, 64)
18446744073709552000
> Math.pow(2, 53) - 1
9007199254740991
> Math.pow(2, 53) + 1
9007199254740992

値としては持つことができているが、253を超える範囲では数値演算が正しく動作しないことが確認できる。 負の数も同様。

> - Math.pow(2, 53)
-9007199254740992
> - Math.pow(2, 53) -1
-9007199254740992
> - Math.pow(2, 53) + 1
-9007199254740991

そもそも何故 int64を使ったのか

GoのAPIサーバとJS間で、文字列よりも言語間で統一されてそうというイメージで時刻はUnixTimeで表現していた。 golangでは time.Time 型に Unix() メソッドが提供されている。 このメソッドは int64 を返していたので、その値をProtocol Buffersでも使用していた。

https://godoc.org/time#Time.Unix

ただし、先程のEcmaScriptのNumber型の仕様上、JSでint64を扱うのはやめておいたほうがよさそう。 関連する問題に、「2038年問題」というのがあるみたい。

2038年問題 - Wikipedia

こういった問題がある以上、API上での時間の扱いとしてUnixTimeを使うのはあまりいい方法ように思えてきた。

ISO8601とRFC3339

ISO8601形式がよさそう。文字列なので先程のような問題は解決できる。 今後はこれを使おうかと思う。

golangtime.Time をISO8601形式に変換する

time.Now().UTC().Format(time.RFC3339)

あとは、go-sql-driver/mysqlのlocの設定もつけておいた ( ?parseTime=true&loc=Local )。

おわりに

常識だったのかもしれない。この機会に知ることができてよかった。

プログラミング言語Go (ADDISON-WESLEY PROFESSIONAL COMPUTING SERIES)

プログラミング言語Go (ADDISON-WESLEY PROFESSIONAL COMPUTING SERIES)