ushumpei’s blog

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

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

【初めてプラグイン作って見た】Markdownのテーブル雛形を作るプラグイン

この記事は Vim Advent Calendar 2017 8日目の参加記事です。

こんにちは。プログラマーをやっています ushumpei と申します。Vim歴は3年くらいです。

今回は、そこそこVimを覚えてきたけれど、 Vimの仕組みをもっと深く理解していきたいと思っている人(自分)向けVim script勉強しました的な記事を書かせていただきました。自分がプラグインを作って見た時に感じたことがつらつらと書いてあります。

プラグインVimversion 8.0 で書きました。もし試していただけた方で、動かないよ!ということがあれば、大変恐縮ですがその旨コメントしていただけると幸いです。(ごめんなさい!バージョンごとの文法の違いは追えてません)

目次

動機

基本的な操作は覚えたけどもっとVimに関して詳しくなりたいというのが動機です。聞いた話によると、 Vim知るにはVim script書くのが早い ということなので勉強し始めました(すごい人たちは書いているし)。せっかくなのでplugin作ってみようと思い、今回はMarkdownのテーブルを作るプラグインを作成することにしました。同じ機能のプラグインはもうすでにいくつかありますが、世の中ほとんどのものはすでに作られているのでしょうがない、ということで練習のつもりで作りました。

リファレンス

基本的な文法については以下のリンクにお世話になりました。

作るもののイメージ

インタフェースとしては :Mdtable コマンドというものを作っていこうと思います。 Markdown table の短縮です。引数に行数、列数を渡すとその行数と列数を持ったテーブルをカーソル位置以降の行に挿入します。

"入力
:Mdtable 2 3

"出力
|   |   |   |
|:--|:--|:--|
|   |   |   |
|   |   |   |

またこのコマンド入力を補助するキーマッピング mdt もデフォルトで設定するようにします。

なので学べることとしては、 コマンドの定義と引数の渡し方キーマッピングカレントバッファへの書き込み引数チェック です。

書いて見た

書いて見ましたushumpei/mdtable-vim。ソースは2つだけで、ディレクトリ構成は以下のようになっています。

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

試すには ~/.vim/plugin/ ディレクトリで git clone git@github.com:ushumpei/mdtable-vim.git とかしてもらえる嬉しいです( :echo &rtp でどこに置けばいいかわかるそうです )。 dein.vim とかを使っている方は call dein#add('ushumpei/mdtable-vim') とか書いてもらえてもやっぱり嬉しいです。

追記: 2017/12/09: Vim 8の場合、上記の方法ではなく、~/.vim/pack/mdtable/startディレクトリを作成して、その中でgit clone git@github.com:ushumpei/mdtable-vim.gitしてもらえればいいです。プラグインの配置についてまとめました: 自作Vimプラグインの置き場所と置き方 - ushumpei’s blog

それぞれのファイルの説明

  • autoload/mdtable.vim: 実際の処理を記述しています。 行数、列数を引数にテーブル文字列を生成する関数 と、 テーブル文字列生成関数を呼び出してバッファに書き込む関数 の2つだけです。
  • plugin/mdtable.vim: インタフェースを記述しています。上記の関数をコマンド、キーマップとして登録しています。

f:id:ushumpei:20171203120943g:plain

学んだことの詳細

コマンドの定義と引数の渡し方

if !exists(":Mdtable")
  command -nargs=* Mdtable :call mdtable#write(<f-args>)
endif

:Mdtable を使えるようにするには command でコマンドを定義するのですが、「引数2個だから -nargs=2 だ!」とか書いて若干ハマりました。 :help command-nargs をちゃんと読んでおけばよかったです。。。

キーマッピング

mapの設定方法は複雑に感じました、 :help 41.12 のマップの箇所で説明されているものを真似して書いていきました。

if !hasmapto('<Plug>Mdtable')
  nmap mdt <Plug>Mdtable
endif
nmap <Plug>Mdtable :Mdtable 
  • hasmapto: <Plug>Mdtable に対してマッピングが行われているか確認
  • 2行目はマッピングがなければデフォルトとしてmdtでマップ
  • 4行目は <Plug>Mdtable:Mdtable にマップ

このマップが二回行われる必要性が、ユーザーが独自にマッピングする場合の考慮、だということだとなかなか分からなかったです。<Plug> を使う意味はユーザーが独自にマッピングするためだとリファレンスにも書いてあるんですけどね!

カレントバッファへの書き込み

これは本当にいい方法だったのかわかっていないのですが、以下のように executenormal を使って記述しました。

" 変数tableにはマークダウンテーブル文字列が入っています
execute "normal o" . table . "\<Esc>"

バッファをスクリプトから触っている記事を見たりするのですが、まだそのあたり勉強不足です。

引数チェック

function! s:create(rowNum, colNum) abort
...
  if !(a:rowNum > 0) || !(a:colNum > 0)
    echoerr 'Arguments must be positive numbers.'
  endif

関数の引数をチェックして 正の数字以外を弾く ということがやりたかったのですが良い方法が見つけられなかったです。はじめは type 関数を使って見たのですが、全ての引数で弾かれるようになってしまい軽く混乱、結局 引数は文字列で渡ってくる という話でした。最終的に、 数値との比較の際に文字列が数値に変換される こと、 文字列がうまく数値に変換できないときは0になる こと、の2つを使ってゆるいチェックをおこなっています。

Vim scriptは途中でエラー出ても止まらない力強いスクリプトなので、間違った入力でもなんらかの出力をしてくれちゃうみたいです。書くにあたってそのあたりのエラーハンドリングのために tryabort を使って見ました。

感想

Vim少し使える」と思っていたのですが、全然知らないことが多くて驚いています。 プラグインを書いた後に自分の .vimrc を見返してみると、今まで曖昧で済ましていた部分が違って見えてとても嬉しい です。

僕はもともと、Vim自体の使い勝手を変えないように、プラグインは使わないようにしていたのですが、今回プラグインを書いたことで、各プラグインがどのようにVimに影響を与えているのか少しわかるようになったため、これからは多少使っていってもいいという気持ちになりました。

これからのことで言えば、もう少し実際に役に立つ機能をかいて行きたい感じです。このプラグインで言えば、多分テーブルとか最初からサイズ決めうちより調節機能とかあったほうがいいとか思います。(まあでも、他のプラグインを色々触ってみることから始めます)

拙い文章、内容になりました。 読んでいただいてありがとうございます!!!

Vim Advent Calendar 2017