ushumpei’s blog

生活で気になったことを随時調べて書いていきます。

Laravel ざっくり調べた

Laravel、急いで勉強する必要ができたので、概要まとめます。あくまでざっくり調べたまとめなので、コード書いたときに誤認に気がつくんだろうなと思います。

phpもしっかり勉強したことがないので、php 7 のインストールや基本的な文法からやり直します。Laravel に関しても完全に手探りなので、フレームワークの基本的な使い方やデファクトなパッケージについて調べます。

Laravel はバージョンごとにかなり差異があるという話を聞きます。なのでバージョンに関しては慎重に選ぶべきだとは思うのですが( LTS などを選択した方が合理的だと思うのですが )、大枠をつかめればいいかなと思っているので、せっかくだし現在最新の Laravel 5.6 についてここでは書きたいと思います。

php

php は対応しているサーバーにおけば動くスクリプト言語で、リクエストとかセッションとか変数にアクセスできて便利だなーくらいの認識です。言語の概要ってどう書けばいいのかわかりません。

とりあえず小さなコードを書きつつ、「インストール」、「基本的な文法」、「デバッグ」、を学びまず軽度な修正をできるようにし、「ファイルの取り扱い」、「外部パッケージの取り扱い」を学んで機能の追加ができるようになればいいなと思います。

インストール

php 7.1 以上をインストールしたいと思いましたが、私の環境では macOS を High Sierra にバージョンアップしたからなのか、すでに php 7.1 が入っていました。それ以前の macOS を使っている場合は php -v でバージョンを確認の上 7 以下が入っていたら、Homebrew などでインストールするといいかもです。

$ brew install php
$ vim ~/.bashrc
# 以下を追記
export PATH="$(brew --prefix php)/bin:$PATH"
$ source ~/.bashrc

基本的な文法

ここの抜粋です。自分が見直しやすいようにメモしておきます。

index.php ファイルを作って、以下を書き込みます。

<?php
echo 'hello';

php + ファイル名でコードの実行ができます。

$ php index.php
hello

このファイルを編集しながら文法練習していきます。行末のセミコロンは必須みたいです。また、 php -a で対話環境の起動ができるみたいです。

  • 変数: 基本的に $変数名 = 値 で宣言。型は色々ある。文字列、数値は他の言語とだいたい同じで困らなそう。
    • 論理型: TRUE とか True とか true とかありで不安。FALSEは同値な値がたくさんある。論理型を取り扱う際は注意する。
    • 配列: リテラルで書けるので基本的に気にしなくていいが、オブジェクトではない(?)ので length とか無い。配列操作に関しては色々関数があってややこしそう。
    • 辞書: $dic = [ 'h' => 'hoge', 'f' => 'fuga']; のようにアロー => でキー・バリューの組みを記述していく。要素の追加は $dic['p'] = 'piuo'; という想像通りの感じ。
    • $_REQUEST, $_POST などのスーパーグローバル変数は、そんなものがそういえばあるという感じで、存在を覚えておく。
  • 定数: ローカル定数がない!?グローバル定数はdefine(名前, 値)const 名前 = 値 で。const はトップレベルスコープなので関数内でイミュータブルな値が欲しいのなら define を使おうかと思ったが、関数外からアクセスできるようになってしまうので気持ちが悪い。なので再代入に関しては 定数に頼らず読みやすいコードを書く という当たり前の結論。多分使い方的には環境変数的なもの。
  • 関数: function 関数名(変数型 $変数名1, 変数型 $変数名2...): 戻り値型 で宣言。型情報かけるようになった(書かなくても動くは動く)。return で戻り値を返す。変数のスコープは基本的に関数内で閉じている。デフォルト引数、可変長引数ありだけど順番に気をつける。参照渡しもあるけど今はあまり気にしない。
    • 可変関数: function hoge() { … } があれば "hoge"() が実行できるらしい。リフレクションみたい、便利だと思う。
    • クロージャ: 無名関数。function () { ... }。親のスコープから値を引き継ぐには専用の構文がいるので、安心感ある。
  • 演算子: 参照代入 $a = &$b は読むとき気をつける。比較演算は等号も不等号も = の数が増えるほど厳密(型チェックが入るかどうか)と覚える。バッククォートでくくると実行演算子と呼ばれシェルコマンドの実行ができるらしいが、覚えておかなくて良さそう?どうなんだろう?加算子/減算子は使える。 文字列結合は .、配列結合は +
  • 制御構文: if について気にすべきところは else ifelseif 共に可ということ。ブラケットではなくコロンを使った記法も可能でその場合は elseif のみ。コロンの時は endif を書く。whileforforeachswitch もコロンによる記法に関して同様だが、switch はブロック <?php ... ?> を分割した時に case のインデントでエラーが出ることがある。また、switch== での比較を行う。break は制御構文の終了、continue はその回のループを終了。declare はディレクティブの宣言で、タイプヒントの厳密型チェックとか有効にできる。
    • ファイル読み込みに関して、 requireinclude と違ってエラーを投げる。_once つけると読み済みのファイルは読み込まない。
  • class: class ClassName { ... } で定義。__construct でコンストラクタ。 プロパティ、メソッド共にアクセス制御修飾子(public, protected, private)をサポートしているし、省略はできない(var では書けるけどその意味はあまりなさそう)。new ClassName()インスタンス化。フィールドへのアクセスは ->static で静的なメソッドを宣言でき、アクセスは ClassName::メソッド名 のように書く。静的なプロパティはconst で定義できる。 クラス内での静的フィールドは self から、非静的フィールドは this からアクセス。クラスパスを解決させることができる、オートロードという仕組みもあるらしい。繼承は extends でできるのは単一繼承。メソッドのオーバーライド可能、parent::メソッド名 で呼び出し。抽象メソッド abstruct、インターフェース interface もある。トレイトは静的、非静的メソッドもプロパティも持てる、宣言は traitトレイト名 で、使用する際は use トレイト名。無名クラスも使える。オーバーロード が他の言語と異なる意味で使われているので注意。オブジェクトはフィールドに対して反復処理 foreach ($obj as $k => $v) が書ける。
  • 名前空間: namespace 名前空間名 で宣言。
  • 例外: try, catch, finnaly で対応。catch (ExceptionA | ExceptionB e) のように複数例外をキャッチできる。

デバッグ

ちょっとまだわかっていないです。var_dump 引数の中身を見せてくれます。配列とかはエコーしても Array だけなので必要だと思います。詳細度は落ちますが print_r もなかなかいいみたいです。

$arr = [
  'hoge',
  'fuga',
  [
    'hoge',
    'fuga'
  ]
];

という配列に対してそれぞれの関数を適応して見ます。

print_r

Array
(
    [0] => hoge
    [1] => fuga
    [2] => Array
        (
            [0] => hoge
            [1] => fuga
        )

)

var_dump

array(3) {
  [0]=>
  string(4) "hoge"
  [1]=>
  string(4) "fuga"
  [2]=>
  array(2) {
    [0]=>
    string(4) "hoge"
    [1]=>
    string(4) "fuga"
  }
}

print_r の方が人間的には読みやすそうですね。(読みやすさが大切かどうかは時と場合によりますが)

ファイルの取り扱い

php でコードを書くときは、とりあえず <?php とファイルの先頭に書き、拡張子を .php にすれば良さそうです。 <?php の閉じタグに関しては書くと予期せぬ挙動が起こることがあるため省略するのが流儀のようです。

ファイルの読み込みは include, include_once, require, require_once で行うことができます。 dirname(__FILE__)スクリプトファイルのディレクトリを参照してくれているため、つなげて相対パスrequire_once(dirname(__FILE__).'/hoge/fuga/piyo.php') のように書くことができるようです。現在位置を明示しないまま ../hoge などのパスを書いた場合、そのファイル自体が別の場所から読み込まれた際にパスがずれてしまうためしっかり書いたほうが良さそうです。

これらの理解も若干微妙ですが、それはさておき php のパッケージ管理システム Composer を使うとオートロードと呼ばれる、ファイルの自動読み込み機能を使うことができるみたいです。

外部パッケージの取り扱い

パッケージの依存性管理ツールに関して。ちょっと前に触ったときは pear を使ったような気がしたのですが、近年は Composer が主流なのでしょうか?確かに上で書いたオートロードなどの機能が魅力的です。パッケージの検索は、https://packagist.org を利用すると良さそうです。

インストールについては Laravel の説明の箇所に書きます。設定ファイルもcomposer.json だし、composer init でパッケージを作成できるみたいで、結構 npm っぽい気がします。パッケージの追加は composer require です、—dev フラグもあり、開発環境のみ依存しているパッケージを管理することができます。

Laravel

ここでは「インストール」、「フレームワークの構成」、「ツール」、「テスト」、「デファクトなパッケージ」を学んで Laravel でプログラミング始める基礎知識がわかればいいなと思います。

インストール

今のところバージョン 5.6 が最新のようなのでそれをインストールします。日本語版ドキュメントを参照しつつインストールをして行きます。 (若干英語版と日本語版のサイトのトップページが違ったり?)

Composer

Laravel はライブラリ管理ツール Composer を使って入れるようです。とりあえず Composer 入っていなかったのでインストールします。

こちらを参考にインストールします。Laravel のドキュメントではグローバルインストールが推奨のようです。

一応 Homebrew (brew install homebrew/php/composer) でもいけました。(が php の依存性がなくなったため必要なら自分で入れてくださいというメッセージが表示されました、という解釈であっている?)。最近の macphp 入っていると思うので気にしないで大丈夫そうです。(問題あったら是非教えてください)

Laravel インストーラ

$ composer global require "laravel/installer"

laravel コマンドを使用できるように、 .bashrc などにパス追記して、再読み込みします。

$ vim ~/.bashrc
# 以下の内容を追記
export PATH=$HOME/.composer/vendor/bin:$PATH
$ source ~/.bashrc
$ laravel -v
Laravel Installer 2.0.1

無事 Laravel コマンドが入りました。

laravel new apps_name

アプリの雛形を作成してみます。何にするか迷いますが、とりあえず、ユーザーがいてログインとか何かしらの投稿とかがあって、という感じがいいかなと思います。

$ laravel new i_bought_it

結構重いですね、このコマンド、Railsrails new 的な。

中身は以下の感じでした。

$ tree -L 1
.
├── app
├── artisan
├── bootstrap
├── composer.json
├── composer.lock
├── config
├── database
├── package.json
├── phpunit.xml
├── public
├── resources
├── routes
├── server.php
├── storage
├── tests
├── vendor
├── webpack.mix.js
└── yarn.lock

10 directories, 8 files

Node.js 関連のファイルが入っているんですね。この辺りは「フレームワークの構成」あたりで調べて行きます。

ローカルで起動

とりあえず起動してみます。

$ cd i_bought_it
$ php artisan serve

localhost:8000 でサーバーが立ち上がりました!

f:id:ushumpei:20180305235852p:plain

開発環境

結論としてはビルトインサーバーと、 sqlite で頑張ることにします。

$ touch database/database.sqlite
$ vim .env
# 以下の内容に変更
DB_CONNECTION=sqlite
DB_DATABASE=database/database.sqlite

以下ざっとみた感じです。

Homestead: 開発環境としては、サーバーとかデータベースとか全部入りの Homestead という選択肢もあるそうです。 ただ Homestead は VagrantVirtualBox (またはその他仮想化ツール) が必要らしいので、若干面倒くさい気分になりました。

Valet: Valet というのも開発環境として選択肢に入るようです。 データベース は別途管理する必要がありますが、 ssl の動作確認も起動オプションだけだし、 Homebrew だけで完結するのが素敵です。ただし Mac オンリーなので Windows での開発に加わる場合ちょっとまごつく可能性もありそうです。

Docker: ノリノリで環境構築してたけど Composer から何から全部 docker でいけたんじゃないかということに気がつきました。例えば: Dockerでlaravelの開発環境構築をしてみた (php-pfm,nginx, mysql)

フレームワークの構成

Laravel の構成について整理してみます。

基本的には「サービスコンテナ #」を中心に考えて行けば良さそうです。サービスコンテナに対して様々なサービスを、「サービスプロバイダ #」によって組み立て、登録していきます。これによってアプリケーションの機能を柔軟に変更、管理していく感じのようです。

ファサード #」はサービスコンテナに登録されているサービスへの API を提供するようです。API の直接の使用のほかにも、「コントラクト #」を使用することでコンストラクタやアクションでタイプヒントを指定しておけばサービスコンテナによって DI される仕組みがあるようです。(コントラクトの登録方法がわかってないですが、、、また、自分の理解ではサービスプロバイダで登録したサービスもタイプヒントによって DI されるのかなと思います)

ORM としては 「Eloquent ORM」 が Laravel に含まれているようですが、使用必須というわけでもないようです。クエリビルダーのようなものであるという話です。ローカルスコープなども定義できるようなので、リポジトリーなどの抽象化がいるかどうかは少し悩みます。論理削除(ソフトデリート)を提供しているようで、論理削除自体の賛否は置いておくとして、実際家な思想に好感が持てます。

ルーティングは Route サービスを(ファサードを通して)使って登録していきます。ルートの登録時、またはコントローラーのコンストラクタで、ミドルウェアを適応することができます。ミドルウェアによってコントローラのアクションに対して認証などの機能を追加することもできるようです。ミドルウェアの登録に関しては App/Http/Kernel.php で記載されています。

(ルーティングに関してリフレクションを使っているのか、URI セグメントから自動的にクエリしてくれる「暗黙の結合 # 」のあたりで、変数名とURIセグメントの一致を要求している部分が面白いです。変数名が意味を持っているのは、気がつかないでハマりそう。)

コントローラーは通常のクラスか、 App/Http/Controllers/Controller.php を継承したクラスです。クラスが依存するサービスは、コントローラーのコンストラクタにおいて引数をタイプヒントで与えておけば、サービスコンテナの仕組みが解決してくれるようです。アクションに対しても同様に DI してくれるようです。(Request などが顕著)

フロントエンドに関しては、 Blade テンプレートエンジン、CSS (SASS、LESS)、Vue が Laravel 5.6 の標準のようです。NPM のエコシステムに乗って webpack によって各種コンパイルを行います。テンプレートの描画は view ヘルパーによってコントローラーから行います。

ツール

artisan が開発において基本的なツールになりそうです。必要そうなものとしては、以下のものかなと思います。

  • artisan list: コマンド一覧
  • artisan route: ルーティング関連、特に route:list
  • artisan make: コードの自動生成系
  • artisan tinker: 対話環境

パッケージに関しては composer を使ってインストールしていくのが無難そうです。 フロントに関しては yarn かと思います。

テスト

テストフレームワークとしては phpunit が初めから入っていました。標準的な Unit, Feature の他に Browser テストが行えるようで (Laravel Dusk)、 Selenium のインストールなしにブラウザ自動操作とテストができるようです。保守の場合は重宝しそうです。

ものすごく個人の感想ですが、とりあえず Unit テストは必須で書いていく、Feature はベストエフォートで、Browser は要件次第かなと思いました。

デファクトなパッケージ

色々探そうと思っていたのですが、 Laravel って標準で色々な機能が入っているようです。認証とかページネーションとか。パッケージ自体は github にたくさん上がっているようです。どこで探すのが正解かわかってないですが、ググると以下のサイトで検索できそうです。

感想

印象としてはフレームワークとしての自由度が、構造を容易に変えられるという意味で、めちゃくちゃ高いなと思いました。でも頑張ればしっかり管理できそうな感じです。バージョン間での差異が少し気になるところですが、逆にそれも自由度が高いが故の変更なのかと思います。どちらかといえばフレームワークの使い方を学ぶよりは、サービスコンテナ自体をしっかりコードを追うなり勉強しておけば、バージョン間の差異についていけるような気がします。

Ethereumの勉強: Hello, World! Contract by Solidity (macOS)

https://www.ethereum.org/images/home-title.png

Hello, world!してみようと思います。

以下のリンクを参考にしました。

動機

geth が v1.6.0 から solidity のコンパイルをサポートしなくなったけれど、あまりそれについて触れているドキュメントがなかったので色々苦労したため、一応この時点ではこれでいけたよ!ということを残しておきます;

  • 環境: macOS High Sierra 10.13
  • 書いた日付: 2018/02/13
  • geth: 1.7.3-stable
  • solc(solidity): 0.4.19+commit.c4cbbb05.Darwin.appleclang

用語

  • geth: go 言語で書かれた Ethereum クライアント
  • コントラクト: Ethereum 上で動作するコード
  • solidity: Ethereum 上でコントラクトを記述するための言語またはコンパイラ (solidity で書いたプログラムを実行環境である Ethereum Virtual Machine で解釈できるようにコンパイルする)

Ethereum について

ここで言う Ethereum はブロックチェーン上でチューリング完全なプログラムを動かせるプラットフォームのことらしいです。一般的に知られている仮想通貨の方は ether と読んで区別しています。ブロックチェーン上にコードが追加できて、そこに向けて GAS (コードが使う資源に応じた実行に必要な ether )や引数などを投げるとコードが実行される感じです。

プログラミングする観点から言うと、コードの書き方によって使用される GAS が異なる(コードを実行するための GAS を少なくする最適化スキルが重宝される)ことが重要な気がします。

Install geth

$ brew tap ethereum/ethereum
$ brew install geth
$ geth version

無事インストールできていることを確認できました。

Private チェーンの作成

新しく実験用のディレクトリを作って、そこで geth を起動します。 --dev オプションによって、本来の Ethereum のチェーンではなく自分専用の開発用のチェーンで起動します。

$ mkdir path/to/somewhere
$ cd path/to/somewhere
$ geth --dev --datadir .

INFO がつらつらと出てきたら起動成功のようです。 WARN が出るかもしれないですが Block sealing failed などは --dev のせいなので気にしなくていいようです。

注意: 今回はコントラクトを作成して実行するだけが目的なのでひとまず気にしなくていいですが、--dev で起動した場合、マイニングが行われるのはトランザクションが生成された時のみのようです # 。初めから大量の ether を持ったアカウントが一人登録されており、このアカウントが miner (マイニングを行うアカウント)の役割をふられています。パスワードは空です。

別コンソールから (JavaScript) 対話環境にログインします。基本的にこのインタフェースで作業していくようです。

$ ls
… geth.ipc …
$ geth attach geth.ipc

別コンソールで先ほど実行した geth --dev --datadir . によって geth.ipc と言うファイルが作成されていることを確認できました。geth attach geth.ipc を実行することで対話環境に入れます。

> eth.accounts
["0x75eece28f8ce7b99af2af2324dbb17f5c1aab56e"]

とりあえず今のところやることはないですが、ethminerweb3などをいじって遊んでみるといいと思います。

Install solidity (solc コマンド)

コントラクトを書くために solidity をインストールします。

$ brew install solidity
$ brew link solidity
$ solc --version

結構時間がかかりました。適当なコマンドを打ってみて、ちゃんとインストールされていることが確認できました。

コントラクトの作成

Hello コントラクトを作成していきます。Hello, world! までの流れとしては以下の 3steps です;

  1. ソースをコンパイルして abi (Application Binary Interface), bin (Binary)を取得
  2. 対話環境で eth.contractabi を食わせてコントラクトの雛形を作成、コントラクトの雛形にコンストラクタ引数として bin と 送信者 と gas を与えてインスタンス化する
  3. インスタンスに対してメソッドの呼び出し(今回はcall)を行う

step 1

solidity のコードサンプルがいくつかネットに落ちているのでそれを参考に Hello, world! プログラムを書きました。拡張子は sol が一般的なようです。ファイル名の命名規則などは後々でいいかのとか思いました。

hello.sol

pragma solidity ^0.4.0;

contract Hello {
  function say() public pure returns (string) {
    return 'Hello, world!';
  }
}

コンパイルします。使いやすく出力フォーマット変えたりできないのかなとか思いますが、ターミナルで加工します。「コンパイル後のコードを json の形で吐き出した後、compilerOutput 変数に代入する」と言う js ファイルを作成します。

$ echo "var compilerOutput = `solc --optimize --combined-json abi,bin hello.sol`" > hello.js
$ less hello.js
var compilerOutput = {"contracts":{"hello.sol:Hello":{"abi":"[{\"constant\":true,\"inputs\":[],\"name\":\"say\",\"outputs\":[{\"name\":\"\",\"type\":\"string\"}],\"payable\":false,\"stateMutability\":\"pure\",\"type\":\"function\"}]","bin":"6060604052341561000f57600080fd5b61014e8061001e6000396000f3006060604052600436106100405763ffffffff7c0100000000000000000000000000000000000000000000000000000000600035041663954ab4b28114610045575b600080fd5b341561005057600080fd5b6100586100cf565b60405160208082528190810183818151815260200191508051906020019080838360005b8381101561009457808201518382015260200161007c565b50505050905090810190601f1680156100c15780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b6100d7610110565b60408051908101604052600d81527f48656c6c6f2c20776f726c6421000000000000000000000000000000000000006020820152905090565b602060405190810160405260008152905600a165627a7a723058206803a6ff4f3b05c9ebd7f5f75edb032cfd89ce2b8177d8653269f91a932178720029"}},"version":"0.4.19+commit.c4cbbb05.Darwin.appleclang"}

solc のオプション abi, bin は 出力される json に含める内容です。

対話環境でコードをロードします。ここまでくればコンパイルは OK と見ていいのだろうか?

> loadScript('./hello.js')
true
> compilerOutput
{
  contracts: {
    hello.sol:Hello: {
      abi: "[{\"constant\":true,\"inputs\":[],\"name\":\"say\",\"outputs\":[{\"name\":\"\",\"type\":\"string\"}],\"payable\":false,\"stateMutability\":\"pure\",\"type\":\"function\"}]",
      bin: "6060604052341561000f57600080fd5b61014e8061001e6000396000f3006060604052600436106100405763ffffffff7c0100000000000000000000000000000000000000000000000000000000600035041663954ab4b28114610045575b600080fd5b341561005057600080fd5b6100586100cf565b60405160208082528190810183818151815260200191508051906020019080838360005b8381101561009457808201518382015260200161007c565b50505050905090810190601f1680156100c15780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b6100d7610110565b60408051908101604052600d81527f48656c6c6f2c20776f726c6421000000000000000000000000000000000000006020820152905090565b602060405190810160405260008152905600a165627a7a723058206803a6ff4f3b05c9ebd7f5f75edb032cfd89ce2b8177d8653269f91a932178720029"
    }
  },
  version: "0.4.19+commit.c4cbbb05.Darwin.appleclang"
}

abi, bin を含んだ感じのものが表示されました。

step 2

コントラクトを作成して実行してみます。abi は JSON.parse, bin は頭に '0x' がついてないので代入時につけてあげます。

> var abi = JSON.parse(compilerOutput.contracts['hello.sol:Hello'].abi)
> var bin = '0x' + compilerOutput.contracts['hello.sol:Hello'].bin
> var contract = eth.contract(abi)
> Hello = contract.new({ from: eth.coinbase, data: bin, gas: 100000 })
> Hello
{
  abi: [{
      constant: true,
      inputs: [],
      name: "say",
      outputs: [{...}],
      payable: false,
      stateMutability: "pure",
      type: "function"
  }],
  address: "0x34f2deef0f113982608a8a943c85dc74293daa7a",
  transactionHash: "0xc81c82a8a3825a5cfef35122f06036ad220601a53f38e029a1a00ff132d3187b",
  allEvents: function(),
  say: function()
}

addressが振られているのでOKみたいです。

注意: 実は何回か試行錯誤していて、アカウントのパスワード聞かれたり、Hello インスタンスに address が振られないという現象がありました。 eth.coinebase のアカウントはパスワードが空で、 address 振られないときは miner.stop() & miner.start() とマイニングを再起動させたりしたら動作するようになりました。(なぜだろう…)

step 3

Hello.say に対して call を実行します。

> Hello.say.call()
"Hello, world!"

ようやく出ました!

まとめ

感想

  • geth は一番使われているという割にはドキュメント散在していたり、動作不安定だったりしていて、そのあたりの仕事はあるのではないかと思いました。
  • geth --dev だと miner がよくわからないルールで動いているようなので、genesis.json を使ってプライベートネットワークを立てた方がいいかもしれないです。
  • solidity に関しては solc でコンパイルするより Remix という IDE 使うと良さそうな感じらしいです。

Botkitで出てくるThread(スレッド)の概念を理解しようと頑張ってみる

こんにちは。

チャットボットフレームワークBotkitを使い始めて困惑したことが、自分なりに解決できたので何か書きます。困惑したのはThreadです。

私が最初に知っておけばよかったと思うことは 一度にユーザーに返せるのは1つのThread内のメッセージThreadはネストさせることはできない ということです。

↓こんなことはできない

user: hello
スレッドA -> bot: hello from thread A!
スレッドB -> bot: hello from thread B!

↓これもできない

user: hello
スレッドA -> bot: hello
user: oh
スレッドB -> bot: he
スレッドB -> bot: llo?
user: oh
スレッドA -> bot: bye!

注意: 「コード読めよ」という話ではありますが、理解できなかったので実験した結果です。気がついたことがあればコメントいただければと思います。

準備の話

いじれる環境を準備します。ネットで記事を探すと「slack + botkit」が目につきますが、サーバーに置くのが面倒なので、開発を自分のラップトップ内で完結させるように準備します。(あ、でもBotkit studioというホスティングサービスがあるのでサーバーに置くのはそれほど面倒ではないかもです)

hubotで言うところのadapterみたいなものの中に、consolebotというnodeのreplで動作するものが使用できるので、まずはその設定を行います。

まずはディレクトリを作ってyarn initなどをした後に、必要なライブラリを追加していきます。

$ yarn add --dev babel-cli babel-preset-env // 新し目の記法を使いたい
$ yarn add botkit // 本体

新し目の記法を使いつつ、コンソールで実行したいのでbabel-cliを追加しています。

package.json に次のように起動コマンドを追加します。

+  "scripts": {
+    "start": "babel-node ./index.js --presets env"
// or nodemonを使うともっとストレスがないかもです
+    "start": "nodemon index.js --exec babel-node --presets env"
+  }

動作確認のために何を言ってもHello worldしか言わないボットを作ります。

index.js

import Botkit from 'botkit';

const controller = Botkit.consolebot({ debug: true });
controller.on('message_received', (bot, message) => {
  bot.reply(message, 'Hello world!');
});
controller.spawn();

yarn startを実行するとボットが起動して対話が開始します。ただしボットからは何も言ってこないのでこちらから話しかけてあげてください。「Hello world!」って言ってくれればOKです。

(Botkit.consolebotの引数でデバッグモードにしていますが、あると結構みにくいのでfalseにしてしまってもいいと思います)

Conversationについて

質問や分岐などの複雑なユーザーとのやりとりのためにConversationというものを作成します。ThreadはConversation(に渡したコールバック)の中で使用することができます。ただしまだ使っていません。詳しくはドキュメントを参照ください。

index.js

...
 controller.on('message_received', (bot, message) => {
   bot.reply(message, 'Hello world!')
+  bot.startConversation(message, (err, convo) => {
+    if (err) throw err
+    convo.say('Conversation start')
+    convo.say('Conversation end')
+  })
 })
...

Threadを意図的に間違えて使ってみる

本題です。以下のようにコードを変更しました。自分としてはsayaddMessageで追加した文章が上から順々にコンソールに表示されればいいなーと思ったのですが全然ダメでした。

index.js

import Botkit from 'botkit'

const controller = Botkit.consolebot({ debug: false })

controller.on('message_received', (bot, message) => {
  bot.reply(message, 'Hello world!')
  bot.startConversation(message, (err, convo) => {
    if (err) throw err
    convo.say('Conversation start')
    convo.addMessage('Thread 1', '1')
    convo.gotoThread('1')
    convo.addMessage('Thread 2', '2')
    convo.gotoThread('2')
    convo.addMessage('Thread 3', '3')
    convo.gotoThread('3')
    convo.say('Conversation end')
  })
})

結果的に表示されたのは以下の文章です。

BOT: Hello world!
BOT: Thread 3
BOT: Conversation end

ここで何が起こっているか考えてみることにします。このコードによっていくつかのスレッド(default, 1, 2, 3)にメッセージが追加されました。それぞれ以下のようになっています。

  • default: ['Conversation start']
  • 1: ['Thread 1']
  • 2: ['Thread 2']
  • 3: ['Thread 3', 'Conversation end']

bot.replyで返されたConversationの外でのメッセージ「Hello world!」は考慮しないとして、ユーザーに 一度に返せるメッセージは1つのThread内のメッセージ であるとするならば、Conversationが最終的に位置している「3」Threadのメッセージが返されているのでは?と考えることができます。

色々考えてみる

それにしても、convo.say('Conversation start')も表示されないのはかなり混乱しました。sayaddMessageの違いはThreadを指定しないことだけだということで、てっきりsayはいつでも表示されると思っていたからです。このメッセージはdefaulに追加されているので、Conversationのコールバックが終わった後の最終的な位置は「3」Threadのため表示されないということでした。つまり Conversationのコールバックが終わった時に位置している最終的なThreadの内容がユーザーに返される と考えることができるのではないでしょうか?

また、チャットボットの特性かもしれませんが、だいたいの処理がユーザー駆動になっています。ユーザーの入力があったときにcontrollerでメッセージを受け取るとコールバックが起動されて、各Threadにメッセージが追加されます。あとでわかったことですが Threadに追加したメッセージは原則変更できない ということも意外と重要なことかもしれません。askaddQuestionなど、Conversation内でユーザーからの入力を再度受け付けるためのメソッドが存在しますが、この 入力結果を使用して動的にメッセージを生成するにはaskaddQuestionのコールバック内で呼び出す必要がある ことに注意しなければいけないと思います。(特に{{vars.hogehoge}}や、convo.extractResponseの値など)

Threadで色々遊んでみる

一応、ある程度の使い方がわかったのでどんなことができるか、色々遊んでみることにしました。

index.js をユーザーの入力から動的に数珠つなぎのスレッドを生成するようにする(全体)

...
    const loop = message.text
    for (let i = 1; i < loop; i++) {
      convo.addMessage(`Thread ${i}`, `${i}`)
      convo.addQuestion(`Do you wanna go to thread ${i + 1}?`, [
        {
          pattern: bot.utterances.yes,
          callback: (res, convo) => {
            convo.gotoThread(`${i + 1}`)
          }
        },
        {
          pattern: bot.utterances.no,
          callback: (res, convo) => {
            convo.gotoThread('complete')
          }
        },
        {
          default: true,
          callback: (res, convo) => {
            convo.repeat()
            convo.next()
          }
        }
      ], {}, `${i}`)
    }
...

index.js を前のスレッドを再利用かつ、すぐに戻って来れるようにする(全体)

...
    const loop = message.text
    let jump // ここと
    for (let i = 1; i < loop; i++) {
      convo.addMessage(`Thread ${i}`, `${i}`)
      convo.addQuestion(`Do you wanna go to thread ${i + 1}?`, [
        {
          pattern: bot.utterances.yes,
          callback: (res, convo) => {
            convo.gotoThread(jump || `${i + 1}`) // ここと
          }
        },
        {
          pattern: bot.utterances.no,
          callback: (res, convo) => {
            convo.gotoThread('complete')
          }
        },
        {
          default: true,
          callback: (res, convo) => {
            convo.repeat()
            convo.next()
          }
        }
      ], {}, `${i}`)
    }
...
    // ここら辺
    convo.addQuestion('Which thread do you like?', [
      ...((l) => {
        const arr = []
        for (let i = 1; i < l; i++) {
          arr.push({
            pattern: `${i}`,
            callback: (res, convo) => {
              jump = `${loop}`
              convo.gotoThread(`${i}`)
            }
          })
        }
        return arr
      })(loop),
      {
        default: true,
        callback: (res, convo) => {
          convo.gotoThread('complete')
        }
      }
    ], {}, `${loop}`)
...

Threadは再利用できる形で作っておく のがポイントだと感じました。あと convo.gotoThreadは基本的にaskaddQuestion内のコールバックでしか使わない。

まとめ

  • 一度にユーザーに返せるのは1つのThread内のメッセージ
  • Threadはネストさせることはできない
  • Conversationのコールバックが終わった時に位置している最終的なThreadの内容がユーザーに返される
  • Threadに追加したメッセージは原則変更できない
  • 入力結果を使用して動的にメッセージを生成するにはaskaddQuestionのコールバック内で呼び出す必要がある
  • Threadは再利用できる形で作っておく
  • convo.gotoThreadは基本的にaskaddQuestion内のコールバックでしか使わない。

感想

  • めちゃくちゃ煩雑で雑多な内容になりました。askとかaddQuestionの説明記事を書いた方が自分と世の中のためになった気がする
  • なんかやったことをとりあえず並べていっているせいか、記事がチュートリアルっぽくなりがち
  • 結局記事のターゲットはBotkitで複雑なことをしたいと思って色々やってよくわかんないってなった人(自分)
  • 「仕組み上何ができないか?」ということがはっきりわかると大変助かるので、そういう部分を探って行きたいと思います。
  • マルチプラットフォーム前提のチャットボットフレームワークがあれば知りたい。コアの部分とインタフェースがしっかり別れていて、コアを使いまわせるものが欲しい。。。messengerとLINEとslackで同じチャットボットと対話できるとめちゃくちゃ広がりそう。チャットプラットフォームがブラウザで、ボットがWebページみたいな世界。
  • チャットボットに将来性をすごく感じているので何か仕事があればとか思う日々を送っています。

Apollo Clientを使ってみるチュートリアルのようなもの

React #1 Advent Calendar 2017 12日目の記事になります。

GraphQL!(こんにちは)

この記事はGraphQLのライブラリ React Apollo を使って、ReactでGraphQLを触ってみようという内容です。

GraphQLどうなんでしょうか? 運用で使って見た系スライドとか公開してくださっている方々がいたりしますが、なかなか敷居が高そうで手が出せていませんでした。主に サーバーの実装がよくわからない、という理由です。

そんな時Full-stack React + GraphQL Tutorialという Apollo公式チュートリアル を見つけて、よしやろう、と思ったのですが、 apollo-clientのバージョンが変わって内容が合わず苦労した ので、そのあたりを自分なりに書けたらと思います。

GraphQL自体に関しては以下のリンクで勉強しました。

目次

Apolloプロジェクトの概要

ApolloMeteor Development Group(JavaScriptアプリケーションプラットフォームMeteorの会社)が開発している GraphQLのオープンソースツールセット (OSSの一群的な)です。サーバー、クライアント両方でGraphQLが使えるようにするためのライブラリがいくつも含まれています。

中でも Apollo Client はReact、Angular、Vue、その他多くのJavaScriptFrameworkでGraphQLクライアントとして使用できるそうで、これの 使い方覚えればいろんなところで便利 なのでは?と思ったりします。(注: 本家のドキュメントがあまり更新されてない印象がありますが)

Apolloで作成されているもの は主に次のようなものがあります、本当はもっとたくさんあります。詳細はリファレンスに書いてあります。または自分のざっくりした記事にも多少書いてあります。

  • apollo-client: サーバーとの間で、クエリ発行、データ取得、キャッシングなどしてくれます。2017/10に バージョン2.0がリリースされて、大幅にコードの分離 が行われました。
  • graphql-tag: GraphQLクエリ文字列をクエリの構文木(GraphQL.js AST format)に変換する gql テンプレートタグが入っています。
  • react-apollo: Apollo Clientをpropsで流すためにルートコンポーネントの親コンポーネントとして使う ApolloProviderコンポーネント 、クエリとコンポーネントからApollo Clientと結びついたコンポーネントを作るための graphql高階コンポーネント が入っています。
  • graphql-tools: サーバー側で、schemaとresolverをから実行可能なschemaを生成する makeExecutableSchema が入っています。

チュートリアルのようなもの

React Apolloを使ってGraphQLクライアントを、Apollo GraphQL Expressを使って簡単なGraphQLサーバーを作成します。注意として、データストアは使いません、メモリに置いておくだけです。WebSocketも使いません。よろしくお願い申し上げます。


ReactApollo

完成品のソースです。

Step 1 -- クライアントを作る

create-react-appコマンドでクライアントの雛形を作成します。

$ mkdir react-apollo-tutorial
$ npm install -g create-react-app
$ cd react-apollo-tutorial
$ create-react-app client

続いてクライアントにApollo Client関係、React Apollo、GraphQL関係、Mock関連のパッケージを追加します。

$ cd client
$ yarn add apollo-client apollo-cache-inmemory apollo-link apollo-link-http react-apollo graphql-tag graphql graphql-tools

react-apollo-tutorial/client/src/App.js に以下の内容を記述します。ただし この時点ではブラウザのコンソールにエラー が表示されます。

import { ApolloClient } from 'apollo-client';
import { HttpLink } from 'apollo-link-http';
import { InMemoryCache } from 'apollo-cache-inmemory';
import gql from 'graphql-tag';
...
const link = new HttpLink();
const cache = new InMemoryCache();
const client = new ApolloClient({ link, cache });
...
const query = gql`query {
  users {
    id
    name
  }
}`;
client.query({ query })
  .then(console.log)
  .catch(console.error);

これは適切なスキーマ、ネットワークがないためです。しかしPOSTが行われたということがエラーからわかります。

Step 2 -- モックのスキーマとネットワークの作成

スキーマの定義 を行います。client/src/schema.js を作成して以下の内容を記述します。これは後ほどサーバー移してそのまま使います。

export const typeDefs = `
type User {
  id: ID!
  name: String
}

type Query {
  users: [User]
}
`;

User型と、その一覧を取得するQueryのusersを定義しました。

次に モックレスポンスを返す ためのclient/src/MockLink.jsを作成します。これはApolloLinkを継承したクラスです。

一般的にApolloLinkを継承したクラスはObservableを返すrequestメソッドを持つ必要があります(チュートリアルで引っかかった部分がモックをつくる部分のここでした。以下のコードはGitHubのIssueから拝借しました)。

import { ApolloLink, Observable } from 'apollo-link';
import { graphql } from 'graphql';
import { print } from 'graphql/language/printer';

export default class MockLink extends ApolloLink {
  constructor(params) {
    super();
    this.schema = params.schema;
    this.rootValue = params.rootValue;
    this.context = params.context;
  }

  request(operation) {
    const request = {
      ...operation,
      query: print(operation.query)
    };

    return new Observable(observer => {
      graphql(this.schema, request.query, this.rootValue, this.context, request.variables, request.operationName)
        .then(data => {
          if (!observer.closed) {
            observer.next(data);
            observer.complete();
          }
        })
        .catch(error => {
          if (!observer.closed) {
            observer.error(error);
          }
        });
    });
  }
}

参考

以下の内容でclient/src/App.jsを編集しましょう。

- import { HttpLink } from 'apollo-link-http';
...
+ import { makeExecutableSchema, addMockFunctionsToSchema, } from 'graphql-tools';
+ import { typeDefs } from './schema';
+ import MockLink from './MockLink';
...

+ const schema = makeExecutableSchema({ typeDefs });
+ addMockFunctionsToSchema({ schema });

- const link = new HttpLink();
+ const link = new MockLink({ schema });

graphql-tools を使ってスキーマの作成(makeExecutableSchema)と、そのスキーマに対するモックのリゾルバの定義(addMockFunctionsToSchema)を行なっています。

この変更により、 ブラウザのコンソールに2名のユーザー が表示されるようになりました。(dataプロパティを持つオブジェクトがブラウザのコンソール表示されているはずです)

Step 3 -- React Apolloを使ってApollo Clientとコンポーネントを紐づける

先ほどはclientインスタンスから直接クエリを呼び出していましたが、 コンポーネントにクエリを結びつけて画面に描画 できるようにします。

まずユーザーを一覧で表示するためのUsersListコンポーネントclient/src/UsersList.jsとして作成します。

import React from 'react';
import gql from 'graphql-tag';
import { graphql } from 'react-apollo';
// import './UsersList.css'; お好みで作成してください

const UsersList = ({ data: { loading, error, users }}) => {
  if (loading) return <p>Loading ...</p>;
  if (error) return <p>{error.message}</p>;

  return <ul className="users-list">
    { users.map(u => <li key={u.id}>{u.name}</li>) }
  </ul>;
};

export const usersListQuery = gql`query {
  users {
    id
    name
  }
}`;

export default graphql(usersListQuery)(UsersList);

UsersListコンポーネントは通常のステートレスなReactコンポーネントで、dataというプロパティの中に入っているusersをリストとして描画するものです。 最後の行の記述、graphql高階コンポーネントによって、usersクエリとUsersListコンポーネントが結びつけられています。

次にclient/src/App.jsを編集して、ApolloProviderにclientを渡すのと、UsersListコンポーネントを描画するよう変更します。

...

+ import { ApolloProvider } from 'react-apollo';
+ import UsersList from './UsersList';

...

class App extends Component {
  render() {
    return (
+     <ApolloProvider client={client}>
        <div className="App">
          <header className="App-header">
            <img src={logo} className="App-logo" alt="logo" />
            <h1 className="App-title">Welcome to React</h1>
          </header>
          <p className="App-intro">
            To get started, edit <code>src/App.js</code> and save to reload.
          </p>
+         <UsersList />
        </div>
+     </ApolloProvider>
    );
  }
}

export default App;

またコンポーネントからクエリを発行するように変更したため、App.jsに書いていた、import gqlquery定数とclient.query(...)関数呼び出し、の部分は削除してしまってください。

Step 4 -- サーバーを立ててモックのレスポンスを返す

$ cd ../ # react-apollo-tutorialディレクトリに戻ります
$ mkdir -p server/src
$ cd server
$ yarn add express apollo-server-express cors body-parser graphql-tools graphql
$ yarn add --dev babel-cli babel-preset-es2015 babel-preset-stage-2 nodemon
$ cp ../client/.gitignore ./
$ cp ../client/src/schema.js src/schema.js

server/src/schema.jsをサーバー用に書き換えます。ここでもまだ先ほどのようにモックを使用するため、graphql-toolsから必要なものをインポートしておきます。

import { makeExecutableSchema, addMockFunctionsToSchema, } from 'graphql-tools';

... // typeDefs

const schema = makeExecutableSchema({ typeDefs });
addMockFunctionsToSchema({ schema });

export default schema;

次にserver/server.jsを作成します。サーバーのエントリポイントはlocalhost:3000/graphqlとして作成していきます。

import express from 'express';
import {
  graphqlExpress,
  graphiqlExpress,
} from 'apollo-server-express';
import cors from 'cors';
import bodyParser from 'body-parser';
import schema from './src/schema';

const PORT = 4000;

const server = express();
server.use('*', cors({ origin: 'http://localhost:3000' }));
server.use('/graphql', bodyParser.json(), graphqlExpress({ schema }));
server.use('/graphiql', graphiqlExpress({ endpointURL: '/graphql' }));

server.listen(PORT, () =>
  console.log(`GraphQL Server is now running`)
);

ポート3000からアクセスしたいため、corsによってCross-origin resource sharingの設定を行っています。またここで、 graphiqlExpress というクエリ実行環境の設定を行っておきます。localhost:4000/graphiqlをブラウザで開くと、現在のサーバーに対してクエリ実行が可能です。

準備がほとんど整ったのでserver/package.jsonにサーバー起動スクリプトを追記します。

{
  "scripts": {
    "start": "nodemon server.js --exec babel-node --presets es2015,stage-2"
  },
  ...
}

yarn startを実行してください。GraphQL Server is now runningというserver.jsで設定したメッセージが表示されていたら完了です。ブラウザでlocalhost:4000/graphiqlを開いてみてください。左のペインで以下のクエリを実行すると、右に実行結果(モックデータ)が表示されます。


graphiql

query {
  users {
    id
    name
  }
}

参考

クライアント側の修正を行います。MockLinkなど使わないものを削除して、HttpLinkを使ってサーバーのエンドポイントとApollo Clientを繋げましょう。

+ import { HttpLink } from "apollo-link-http";
...
- import { makeExecutableSchema, addMockFunctionsToSchema, } from 'graphql-tools';
- import { typeDefs } from './schema';
- import MockLink from './MockLink';
...
- const schema = makeExecutableSchema({ typeDefs });
- addMockFunctionsToSchema({ schema });

- const link = new MockLink({ schema });
+ const link = new HttpLink({ uri: 'http://localhost:4000/graphql' });

ついでにclient/src/schema.jsも削除しておきましょう。

Step 5 -- resolvers.jsを記述する、Mutationもしてみる

更新の前に、まずは今までデータがモックだったので実際のデータを使えるように変更します。server/src/resolvers.jsを作ってQueryの実装を記述します。

let userCount = 2;
const users = [
  { id: 0, name: 'ushumpei' },
  { id: 1, name: 'apollo' },
];

const resolvers = {
  Query: {
    users: () => users,
  }
};

resolversのQueryオブジェクトの構造がschemaのtype Queryの構造と同じになっていることに注意してください(Queryの中にusersが書いてあります)。users配列を宣言し、resolversオブジェクトにQueryオブジェクトを定義して、その中にクエリの関数usersの実処理を記述しました(予想がつくかも知れませんが、後でこの中にMutationオブジェクトも追加します)。

server/src/schema.jsで上で書いたresolversを読み込みます。 addMockFunctionsToSchema を削除し、 makeExecutableSchemaresolvers を渡します。

import { makeExecutableSchema } from 'graphql-tools';
import resolvers from './resolvers';

export const typeDefs = `
type User {
  id: ID!
  name: String
}

type Query {
  users: [User]
}

const schema = makeExecutableSchema({ typeDefs, resolvers });
export default schema;

localhost:4000/graphiqlで確認してみましょう。

query {
  users {
    id
    name
  }
}

=>

{
  "data": {
    "users": [
      {
        "id": "0",
        "name": "ushumpei"
      },
      {
        "id": "1",
        "name": "apollo"
      }
    ]
  }
}

うまく表示されたでしょうか?次は、更新処理を受け付けるために、 スキーマにMutation型を設定します。

server/src/schema.jsの変更

...
type Mutation {
  addUser(name: String!): User
}
...

type Mutationを追加します。受け付けるmutationとしては、addUserという名前で、nameという文字列の引数が必須だとします。その後、userオブジェクトを戻り値としています。(MutationでもQuery同様、オブジェクトを返します、その際は同じようにフィールドを記述することができます)

server/src/resolver.jsの変更

const resolvers = {
  ...
  Mutation: {
    addUser: (root, args) => {
      const user = { id: String(userCount++), name: args.name };
      users.push(user);
      return user;
    },
  }
};

localhost:4000/graphiqlでの確認してみます。

mutation {
  addUser(name: "hoge") {
    id
    name
  }
}

=>

{
  "data": {
    "addUser": {
      "id": "2",
      "name": "hoge"
    }
  }
}

usersクエリで確かめると、Mutationによりuserが一人追加されたことがわかります。

Step 6 -- クライアントからMutationを行う。更新後のデータ制御を行う。

Mutationを行うテキストエリアのコンポーネントを作成します。このコンポーネントはReact Apolloによって引数に関数mutateが渡されるため、それを使って更新内容を記述します。

client/src/AddUser.jsを作成して以下の内容を記述します。

import React from 'react';
import gql from 'graphql-tag';
import { graphql } from 'react-apollo';

const AddUser = ({ mutate }) => {
  const handleKeyUp = (evt) => {
    if (evt.keyCode === 13) {
      mutate({
        variables: { name: evt.target.value },
      });
      evt.target.value = '';
    }
  };

  return (
    <input
      type="text"
      placeholder="New user"
      onKeyUp={handleKeyUp}
    />
  );
};

export const addUserMutation = gql`
  mutation addUser($name: String!) {
    addUser(name: $name) {
      id
      name
    }
  }
`;

export default graphql(addUserMutation)(AddUser);

if文の部分が更新処理になります。mutate関数にクエリの引数nameを渡してます。

AddUserをclient/src/App.jsから読み込みます。

...
import AddUser from './AddUser';
...
class App extends Component {
  render() {
    return (
      <ApolloProvider client={client}>
        <div className="App">
          ...
+         <AddUser />
          <UsersList />
        </div>
      </ApolloProvider>
    )
  }
}

画面にテキストエリアが表示されているかと思います。文字入力後Enterで更新クエリが飛ぶようになりました。しかし画面の更新はリロードが必要な状態です。クエリ発行後に更新を行うよう設定できます。

...
+ import { usersListQuery } from './UsersList';

const AddUser = ({ mutate }) => {
  const handleKeyUp = (evt) => {
    if (evt.keyCode === 13) {
      mutate({
        variables: { ... },
+       update: (store, { data: { addUser } }) => {
+         const data = store.readQuery({ query: usersListQuery });
+         data.users.push(addUser);
+         store.writeQuery({ query: usersListQuery, data });
+       },

mutateにupdateという関数を渡します。 ユーザー一覧を取得するusersListQueryの結果を更新することでサーバーへの再問合せなしにユーザー一覧に新たなユーザーを表示します。 usersListQueryの結果はキャッシュに入っているため、readQueryで配列データを取得し、配列にpushして更新し、writeQueryでキャッシュを更新します(キャッシュというかstoreと思った方がわかりやすいかもしれません)。これで更新されるようになりました。

この更新処理はMutationを発行した画面にしか適応されません。同じページを複数人が見ている時のことを考えて、一覧に関して、定期的にデータを更新するように pollInterval を設定しておきましょう。

client/src/UsersList.jsに以下の設定を追記します。

export default graphql(usersListQuery, {
+ options: { pollInterval: 5000 },
})(UsersList);

これだけで5秒おきにクエリを確認しに行ってくれます。

まとめ

以上で完成です。以下のことを扱いました。

  • Apollo Clientにモックネットワークを設定してレスポンスのテストをする
  • React Apolloを使ってコンポーネントとクエリを結びつける
  • Apollo Srver Expressを使ってGraphQLサーバーを立てる
  • Query、Mutationを定義して、実装する

感想

非常に散らかった記事になってしまい申し訳無いです。ここまで読んでくださってありがとうございます。Full-stack React + GraphQL Tutorial劣化コピーっぽくて申し訳ないです、この記事は参考程度にご確認ください。

この記事ではWebSocket部分は扱っていないし、ApolloClientのいい部分をあまり紹介できていない気がします。とりあえず私がApolloClientで面白いと感じた部分は以下の部分です(主に気にしなければならないであろう、データ更新を気軽に実装できるところ);

  • Mutation後のデータ更新
  • Query発行のインターバル制御: pollInterval
  • キャッシュへのAPI: (readQuery,writeQuery, readFragment and writeFragment)
  • OptimisticUI(楽観的UI描画?)
  • クライアントはバックエンドをブラックボックスとして扱うという考え: the backend as a black box

何かお気付きの点ございましたら、お気軽にコメントください!さようなら!

(おまけ) Full-stack React + GraphQL Tutorialで私が詰まったところの解消方法

MockNetworkの作成

これは本記事に書いてあります

customResolversがcacheResolversに変わっていた

Apollo ClientはGraphQLが階層型であるということを利用して、クエリをサーバーに投げる前に、欲しいデータをすでにキャッシュしていないか確かめることができます。そのために、データを一意に表す値に紐づけてキャッシュしておく設定を書きます。

この設定を記述する場所が、Apollo ClientのオプションcustomResolversからInMemoryCacheのcacheResolversに変更になっていたことに大変困惑しました。

const inMemoryCache = new InMemoryCache({
  cacheResolvers: {
    Query: {
      channel: (_, args) => {
        return toIdValue(dataIdFromObject({
          __typename: 'Channel',
          id: args['id'],
        }))
      },
    },
  },
});

ボットとのやりとりが愉快

WebSocketの設定

Apollo CLientのオプションnetworkInterfaceがなくなったため、ApolloLinkとしてWebSocket込みのLinkを作成します。Subscriptionのみの場合、WebSocketの設定はWebSocket用のLinkとHttp用のLinkを組み合わせる必要があります。

const wsLink = new WebSocketLink(new SubscriptionClient('ws://localhost:4000/subscriptions', { reconnect: true }));
const httpLink = new HttpLink({ uri: 'http://localhost:4000/graphql' });
const link = ApolloLink.split(
  ({ query, operationName }) => {
    const operationAST = getOperationAST(query, operationName);
    return !!operationAST && operationAST.operation === 'subscription';
  },
  wsLink,
  httpLink,
);

という感じです。

参考: addGraphQLSubscriptions is removed from 0.9.0 version?

自作Vimプラグインの置き場所と置き方

先日プラグインを作成してみた記事を公開しましたが、その記事のコメントで tyruさん からご指摘いただいた(とてもありがたいです)、プラグインのインストール場所と方法について整理しておきます。

分類

そもそも自分で書いたVim スクリプトVimに読んでもらうためには次の3つが考えられると思います。

  1. プラグインとして配置する
  2. パッケージとして配置する
  3. プラグインマネージャーを使用して配置する

ここではデバッグのことを考えて自力で配置する方法「1. プラグインとして配置する」「2. パッケージとして配置する」に限定して説明していきます。なので、「3. プラグインマネージャーを使用して配置する」に関しては書きません(プラグインマネージャーのリファレンスを読んだ方がわかると思います。私は dein.vim 使っています)

前提

プラグインリポジトリpluginautoload の2つのソースファイルディレクトリを持っていることを前提とします。自分が作ったプラグイン mdtable を例にすると、次のようになっています。

.
├── autoload
│   └── mdtable.vim
└── plugin
    └── mdtable.vim

プラグインとして配置

Vimには2種類のプラグイン「グローバルプラグイン」、「ファイルタイププラグイン」があります。ここではクローバルプラグインについて説明します。

Vimが起動時に読み込むプラグインディレクトリはシステムごとに異なります。以下の表は:help add-global-pluginから見ることができます。

system plugin directory
Unix ~/.vim/plugin/
PC や OS/2 $HOME/vimfiles/plugin or $VIM/vimfiles/plugin
Amiga s:vimfiles/plugin
Macintosh $VIM:vimfiles:plugin
Mac OS X ~/.vim/plugin/
RISC-OS Choices:vimfiles.plugin

注意: PC や OS/2Windows用なのですね

参考: プラグインの追加 | :help plugin

私が作成したプラグインを配布する場合は以下のようになります。

$ cd ~/.vim
$ mkdir plugin                                                                   # pluginディレクトリがなければ実行する
$ cd plugin
$ git clone git@github.com:ushumpei/mdtable-vim.git

パッケージとして配置

Vim 8から使えるようになったパッケージ機能を使って配置します。パッケージは複数のプラグインをまとめるのにも使用できますが、ここでの例ではプラグイン1つを配置するためにパッケージを作成します。

パッケージディレクトリは~/.vim/packになります。この下にパッケージを置いていきます。パッケージ名のディレクトリを作成し、その下にstartというディレクトリを作ってください。その中にプラグインを入れていきます。パッケージのディレクトリ構成は以下のようになることが:help package-createから見ることができます。

.
├── start/foobar/plugin/foo.vim          " 常にロードされ、コマンドを定義する
├── start/foobar/plugin/bar.vim          " 常にロードされ、コマンドを定義する
├── start/foobar/autoload/foo.vim        " fooコマンドを使用した時に読み込む
├── start/foobar/doc/foo.txt             " foo.vimのヘルプ
├── start/foobar/doc/tags                " ヘルプタグ
├── opt/fooextra/plugin/extra.vim        " オプションのプラグイン、コマンド定義
├── opt/fooextra/autoload/extra.vim      " extraコマンドを使用した時に読み込む
├── opt/fooextra/doc/extra.txt           " extra.vimのヘルプ
├── opt/fooextra/doc/tags                " ヘルプタグ

参考: Vimパッケージを作る | :help package-create

プラグイン作成時に pluginautoload などのディレクトリでコードを分けて作った理由がようやく実感できました。

私が作成したプラグインを配布する場合は以下のようになります。

$ cd ~/.vim
$ mkdir pack                                                                     # packディレクトリがなければ実行する
$ mkdir -p pack/mypackage/start
$ cd pack/mypackage/start
$ git clone git@github.com:ushumpei/mdtable-vim.git

まとめ

Vimはドキュメントがとても充実しているため、大変助かります。なのであまりブログに書いても、と思ったりしましたが、自分の勉強を兼ねてこれからも書きます。