ushumpei’s blog

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

静的 html を BASIC 認証付きで雑に公開する方法

雑なメモです (なんかやばかったら教えていただきたいです)

1. 適当なリポジトリを作成する

$ mkdir private
$ cd private
$ git init

2. html ファイルとかを配置する

$ echo '<!DOCTYPE html><html><head><title>private</title></head><body><h1>I am private!</h1></body></html>' > index.html

3. heroku アプリを作成して、 php のビルドパックを追加する。php プロジェクトだと認識させるために composer.json を作成する。

これ、 php じゃないのに php 使う気持ち悪さがあります、多分違う方法ありそう。

$ heroku create
$ heroku buildpacks:set heroku/php
$ composer init

4. .htaccess.htpasswd を作成する

デフォルトの設定で apache を使用するようになっているけど、Procfileweb: heroku-php-apache2 とか書くほうが確実かも

$ echo 'AuthUserFile /app/.htpasswd
AuthType Basic
AuthName "Restricted Access"
Require valid-user' > .htaccess
$ htpasswd -c ./.htpasswd なんか好きなユーザー名

(名前が .ht* のファイルにはアクセス制限かけてるみたいだけど、リポジトリ.htpasswd 含めるのってどうなのだろう)

5. 必要なものステージしてコミットして heroku に push する。

$ git add .
$ git commit -m 'Initial commit.'
$ git push heroku master

6. 終わり

$ heroku open

PostgreSQL 10 でパーティションをまるっと切り替えてみる

PostgreSQL の 10 では宣言的パーティショニングが使えるようになったそうですね (しかし僕は 10 以前でパーティショニングしたことがなかったので感想がない)

これを使ってデータの一括削除、一括追加を実現するために、パーティションの切り替えについて練習します。

準備

Docker で PostgreSQL 10 を用意します

$ docker run --name postgres -p 5432:5432 -d postgres:10

テーブルの作成

こんな感じのテーブルを作ります。ユーザーがいつどこに居たかをひたすら集める謎のテーブルです。

user_id latitude longitude created_at
integer decimal(11, 8) decimal(11, 8) timestamp

パーティションcreated_atrange で切って見ます。

create database testgres;

\c testgres

create table locations (
  user_id integer not null,
  latitude decimal(11, 8) not null,
  longitude decimal(11, 8) not null,
  created_at timestamp not null
) partition by range (created_at);

パーティションの作成

パーティションは partitions スキーマ以下に作っていきます。一秒刻みで適当なデータを作成します。 20181010 と 20181011 のパーティションを作成しました。

create schema partitions;

create table partitions.locations_20181011 partition of locations for values from ('2018-10-11') to ('2018-10-12');

insert into partitions.locations_20181011 select
  (random() * 10000)::int,
  130 + (20 * random())::decimal(11, 9),
  20 + (20 * random())::decimal(11, 9),
  generate_series('2018-10-12'::timestamp - '1 sec'::interval, '2018-10-11', -'1 sec'::interval)
;

create table partitions.locations_20181010 partition of locations for values from ('2018-10-10') to ('2018-10-11');

insert into partitions.locations_20181010 select
  (random() * 10000)::int,
  130 + (20 * random())::decimal(11, 9),
  20 + (20 * random())::decimal(11, 9),
  generate_series('2018-10-11'::timestamp - '1 sec'::interval, '2018-10-10', -'1 sec'::interval)
;

クエリしてみると、ちゃんとパーティションを使った検索が行われているのがわかります。

explain select * from locations where created_at = '20181010 12:00:00';

                                    QUERY PLAN
-----------------------------------------------------------------------------------
  Append  (cost=0.00..1716.00 rows=1 width=28)
    ->  Seq Scan on locations_20181010  (cost=0.00..1716.00 rows=1 width=28)
          Filter: (created_at = '2018-10-10 12:00:00'::timestamp without time zone)
(3 rows)

パーティションの切り替え

f:id:ushumpei:20181015011230j:plain

以下が概要になります。

  1. 普通のテーブルとして locations_20181011 を作成します。
  2. 現在のパーティション partitions.locations_20181011locations から Detach します
  3. 新しいパーティションとして locations_20181011locations に Attach します
  4. 必要なくなった partitions.locations_20181011 を削除します
  5. (跡片付け) 新しいパーティションとして Attach した locations_20181011partitions.locations_20181011 にリネームします

手順 3, 4 と、なるべく alter table locations をまとめて行うことで、テーブルロック時間を少なくしていこうと思います。

新しく locations_20181010 を普通のテーブルとして public スキーマに作成します。(すでにあるものと被らなければ他の方法でもいい)

ここでは check 制約を追加して、Attach される際のパーティションの制約チェックを先立って行っています。これがないとテーブルロック時間が attach の際に増えてしまうそうです。

create table locations_20181011 (
  user_id integer not null,
  latitude decimal(11, 8) not null,
  longitude decimal(11, 8) not null,
  created_at timestamp not null,
  check (created_at >= '2018-10-11' and created_at < '2018-10-12')
);
insert into locations_20181011 select
  (random() * 10000)::int,
  130 + (20 * random())::decimal(11, 9),
  20 + (20 * random())::decimal(11, 9),
  generate_series('2018-10-12'::timestamp - '1 sec'::interval, '2018-10-11', -'1 sec'::interval)
;

パーティションの切り替えを行います。現在使用されている partitions.locations_20181011 を切り離し、新しく作った locations_20181011 をくっつけます。

begin;
alter table locations detach partition partitions.locations_20181011;
alter table locations attach partition locations_20181011 for values from ('2018-10-11') to ('2018-10-12');
commit;

いらなくなったパーティションの削除、跡片付けとして作成したパーティションのリネームを行います。

drop table partitions.locations_20181011;
alter table locations_20181011 set schema partitions;

以上です。

感想

  • テーブルロック怖い
  • トリガー怖い
  • インデックスも適切に

WebAssembly って?

ブラウザからアセンブリ言語を実行できる仕組みが WebAssembly という理解です (雑魚)。

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

Emscripten

C/C++ から WebAssembly で実行可能なアセンブリコンパイルするツールだそうです。C/C++ に特に思い入れはなく、仕事で使ったことはないですが、例えば C/C++ で書かれたライブラリを JavaScript ライブラリに変換するとかできるのかなーと思います。

C/C++からWebAssemblyにコンパイルする を参考に emscripten をインストールします。(すっごい時間かかりますね)

使う

適当な cpp ファイル (main.cpp) を作成します (これ c といってもいいのでは)

#include <stdio.h>
#include <emscripten/emscripten.h>

extern "C" {
  int main()
  {
    puts("Hello, World");
  }

  int myFunction(int x)
  {
    return ++x;
  }
}

em++ main.cpp -s EXTRA_EXPORTED_RUNTIME_METHODS="['ccall']" -s EXPORTED_FUNCTIONS="['_main', '_myFunction']" を実行します。ここでは

  • クライアント側から関数を呼び出す Module.ccall を使用するために EXTRA_EXPORTED_RUNTIME_METHODSccall を指定します。
  • 呼び出せる関数を EXPORTED_FUNCTIONS で指定します。関数名に _ プレフィックスをつけなければいけないそうです。

以下のファイルが生成されました。

  • a.out.js
  • a.out.wasm
  • main.cpp

index.html を作成してそこから a.out.js を読み込みます。

<html>
  <head>
    <script src="a.out.js"></script>
    <script>
      function callMyFunction() {
        var count = document.getElementById('count')
        var nextCount = Module.ccall('myFunction', 'number', ['number'], [count.innerText])
        count.innerText = nextCount
      }
    </script>
  </head>
  <body>
    <p>Count: <span id="count">0</span></p>
    <button onclick="callMyFunction()">Call C++ Function</button>
  </body>
</html>

しかしこれでは動きません。http 経由で配信しなければいけないそうです。express で配信するようにします。yarn init && yarn add express でサーバーを準備します。index.js を以下のように記述しました。

var express = require('express');
var app = express();

app.use(express.static('public'));

app.get('/', function(req, res) {
  res.sendFile(__dirname + '/public/hello.html');
});

app.listen(3000);

またディレクトリ構造を少し変えます。コンパイルしたものは public 以下に放置しています。

.
├── index.js
├── node_modules
├── package.json
├── public
│   ├── a.out.js
│   ├── a.out.wasm
│   ├── index.html
│   └── main.cpp
└── yarn.lock

サーバーを node index.js で起動すると localhost:3000 でアクセスできるようになます。

f:id:ushumpei:20180619104114g:plain

感想

また何かに入門だけしているやつです

Pro Git 2nd Edition 読んでる

最近手が痛くてプログラミング時間を少々減してて、久しぶりに本でも読もうかという気分になっています。

git ちゃんとわかっていなかったので、 Pro Git 2nd Edition を読み始めました。ちょっと面白いことがあったのでメモします。

前提: git は差分ではなくファイルのスナップショットを保持している

git が既存の VCS (Version Control System) と大きく異なった点として、ファイル変更履歴の管理方法が 変更されたファイルの差分ではなく変更されたファイル全体のスナップショット であることだと書かれています。自分の理解だと、ファイル容量などを考えると変更差分を保持していた方がいいと思いますが、ぶっちゃけテキストファイルが主だしまあスナップショットでも大丈夫なんだろうなー、くらいの軽い感じでした。しかしこのことが結構重要で、 git がめちゃくちゃ速い理由につながっているようです。

リポジトリ内のファイル全体のスナップショットが作成されるのは commit 時で、次の要素が .git/objects 以下にファイルとして作成されます。ファイル形式は blob でファイル名はそのデータのハッシュ値です。

  • blob オブジェクト: 変更後のファイル
  • tree オブジェクト: ディレクトリツリー。変更対象のファイルを持っていたディレクトリに対して、ツリーのノードの参照先 (tree, blob オブジェクトのハッシュ値) を書き換えた tree オブジェクトが作成される。(毎回リポジトリルートの tree は更新される)
  • commit オブジェクト: 新しく作成されたリポジトリルートの tree への参照と、parent commit オブジェクトへの参照と作成者情報やコミットメッセージ。

言葉だとややこしいですが実物は以下のようになります(ハッシュ値は適当です)。

blob オブジェクト: .git/objects/f5/83c304ea36b6fa554eb01381e781b04e45477f

# Pro Git

URL: [https://git-scm.com/book/ja/v2](https://git-scm.com/book/ja/v2)

tree オブジェクト: .git/objects/20/4bfe00b89a265e7c16e8688a90dfb86e52c5eb

100644 blob f583c304ea36b6fa554eb01381e781b04e45477f README.md

commit オブジェクト: .git/objects/31/ba323616cc8cbc543882c5a4eef6aa95eef803

tree 204bfe00b89a265e7c16e8688a90dfb86e52c5eb
author ushumpei <mail@example.com> 1528220889 +0900
committer ushumpei <mail@example.com> 1528220889 +0900

Initial commit.

(.git/objects 以下のファイルは git cat-file コマンドに -p オプションをつけてハッシュ値の頭から 6 文字を引数にして実行すると、中身を閲覧することができます: .git/objects/f5/83c304ea36b6fa554eb01381e781b04e45477f なら git cat-file -p f583c3)

要するに?

要するに私が面白いと感じたのは、特定のコミットに入っているファイルを取り出したいときは、commit オブジェクトのハッシュを使って、

commit オブジェクト -> tree オブジェクト -> (ツリーの探索) -> blob オブジェクト

という風に取り出してくれるので、めっちゃ速い、すげー、ということです。これは過去のコミットでも、分岐してある程度進んだブランチのコミットでも同じように行われます。(多分)

感想

「git 速い」って何と比較して、というと svn なのですが、ベンチマークとか取っていないので (どう比較するか謎ですが) 怒られそうですね。「考え方の変化で処理が変わった」ということにテンションが上がっただけです。

React 360 で vnc クライアントを作って Oculus Go のブラウザから自分のPCを見る (未完成)

注意: チラシの裏です

こんにちは

Oculus Go に Mac 用のリモートデスクトップがなかったので (現在: 2018/05/31) なんとかできないかと思って色々やっています。理想的には HMD つけながら無線キーボードでお布団に入りながらコーディングしたいという思いがあります。

でも全然完成までの道のりが見えないので、現状の整理のためのメモを書きます。

(これ以降進展なし)

なんで作ってるんですか?

Oculus Go で使える Mac 用のリモートデスクトップアプリが見つからなかったためです (有料のやつある?)。ブラウザで画面共有できる Web サービスもあるのですが、仕事のコードとか書くことを考えるとローカルネットワークで完結するのが気分的に一番良いんじゃないか?と思ったため作り始めました。(自作すること自体はそれはそれでリスクですが)

Mac にはデフォルトで「vnc」と呼ばれる (?) リモートデスクトップのサーバーが入っているため、それ用にクライアントのコード書いたら良いんじゃないか、ということで vnc 周りを調べつつ作っています。

なんで React 360 なんですか?

  • Android studioAndroid Mobile SDK: C++ 混じってて読めなかった
  • Unity: 入ってるけど使ったことない
  • React 360: 既存の知識使ってできる

という消極的な理由からです。 1 週間くらいで終わらせたかったのでがっつり学習する必要があるものは避けました(終わってないけど)。今考えると Unity は空間すでにあるし知見も多いため一番良い気がしてます。

どんな感じで作りますか?

  • Mac
    • 8080 port: http で React 360 のページを返すサーバー
    • 5900 port: 標準の vnc サーバー
    • 5901 port: websockify というライブラリを使用して WebSocket の 5901 への接続と、 vnc の 5900 への接続をつなぐ

という構成を元に、

  1. 同じローカルネットワーク内のブラウザから WebSocket で Macvnc サーバーと接続
  2. vnc サーバーから送られてくる画面データを React 360 内の canvas に描画
  3. ブラウザのキー入力やポインター移動イベントをサーバーに通知する

していくように作っています。

できたこと

  • React 360 の平面オブジェクト (Plane) に canvas を貼り付ける
  • RFB 3.8 プロトコル (vncプロトコル) を実装してサーバーとの接続を確立する
  • ブラウザでサーバーから画面データを受信する
  • 受信したデータを canvas に描画する

できてないこと

  • ブラウザのイベントをサーバーに通知する
    • ブラウザ -> サーバー への通信は、初めはいらないと思っていたんですがユーザーのサインインが必要なので、ないとログイン画面を延々とみていなければならなくなります
  • 画面が全更新になっているので差分更新で済むようにしたい

どうします?

  • RFB プロトコルのコードを整理する
    • RFB プロトコルが接続を確立するまでに複数回のやりとりが必要になるのですが、全てのメッセージを長い onmessage で受け取っていろんなフラグで if else と長々と書いているので破綻してる
    • ドメインの知識が増えて来たのでちゃんと書いてみたい
    • Redux を React 以外の文脈で使ってみたい
    • オレオレイベント emitter 消す
  • React 360 を一旦やめる
    • canvas を貼り付けた Plane の取り扱いが不安定すぎるので単純な html として書き直します (PC めちゃくちゃ熱くなるし)
      • React でも良い気がするけど
        • ブラウザ vnc クライアントの vnc.js とかあるっぽいけど

一応リポジトリ: GitHub - ushumpei/VncClient: Vnc client for browser

感想

遊んでないで仕事探す

既存の Laravel プロジェクトに自動構文チェックを軽く入れる

Laravel 始めました。今回はコードの自動構文チェックをすごく軽く入れる話。ゆくゆくは CI に含める予定だけどまだその時間と根回しがないので簡単にやります。

どんな効果を狙っているか?

  • レビューにおいて構文に関する指摘が大半を占めているのでその時間を削減する
  • 構文に関する規約が暗黙知化しているので (レビューで指摘が多く上がる原因のひとつ)、規約を管理できる構成にする
  • 書くのが面白そう!

どんなものを作るか?

  • git のプレコミットフック (git/hooks/pre-commit) で追加/変更したコードの構文チェックをして、違反していたらコミットできなくする (コミットできない、はやりすぎか?)
  • 違反している箇所については構文チェックライブラリのエラーを出す (変更行だけチェックできないか? -> 今回はできなかった...)
  • 緊急時のためにフックを無視する手段も作っておく(すでにプレコミットフックとして仕組みがある? -> --no-verify option)
  • プレコミットフックがない環境に影響を与えない (まあ多分 pre-commit 配置しなければ良いだけと思う)
  • 構文チェックのルールはファイルで管理できるようにする (構文チェックライブラリが標準で備えているはず)
  • とりあえずチェック対象は、jsphp (当面は拡張子によって判定)

準備

使うものとしては以下を考えています

  • phpcs (PHPCodeSniffer)
  • eslint

Laravel と言っていますが、git 使ってれば応用効くと思います。それぞれ composer , npm (or yarn) で開発環境の依存ライブラリとして入れます。

(これらがそもそもグローバルインストールされている状態だとどうなるんだろう…?それも忘れずに確認しなきゃ…)

pre-commit 作ってみる

まずは何も構文チェックライブラリ入れずにフックだけ作成してみます。処理の流れとしては、 git commit を実行した直後に、pre-commit が呼ばれ、スクリプトの戻り値(?) が 0 以外の時に commit が停止されるというものです。

.git/hooks/pre-commit

#!/bin/sh

echo hoge
exit 1

また、スクリプトの実行権限を 755 に変更しておきましょう。

$ touch fuga
$ git add .
$ git commit
hoge

とりあえず commit 時にフックが実行されるのがわかりました。あとは処理を書いていきます。

対象ファイルの抽出

git のコマンドを使用して HEAD との比較で、追加、更新があったファイルを取得します。コードは以下のようになります。

#!/bin/sh

# Get against commit hash to compare.
if git rev-parse --verify HEAD >/dev/null 2>&1
then
    against=HEAD
else
    # Initial commit: diff against an empty tree object
    against=4b825dc642cb6eb9a060e54bf8d69288fbee4904
fi

# Redirect output to stderr.
exec 1>&2

# Get added and modified files.
files=`git diff-index --cached --name-only --diff-filter=AM ${against}`

echo ${files}

比較結果は files に、スペース区切りの文字列で入ってきます (配列として取り扱うかどうかは悩みましたが、特に必要ないので git diff-index ... の戻り値をそのまま使います。)

ここでは --diff-filter=AM によって追加 (A)、更新 (M) のあったファイルのみを抽出しています。削除やリネームしただけのファイルはチェックしません。 (削除ファイルやリネームファイルのチェックは、自分が書いたわけでも無いファイルの構文を直せということになり、プロジェクトの整理作業に負のモチベーションを与える気がします。)

exec 1>&2 は標準出力をエラー出力にリダイレクトしています。このスクリプトで出力が行われることは、即ちエラーだからです。 (と偉そうにいってますが、この行までは全て .git/hooks/pre-commit.sample から盗んできたものです。)

対象ファイルを拡張子で分類

ここまでくると特別なことはありません。シェルスクリプトを書くだけです。

...
# ↓チェック対象のファイル取得後の処理

# Check syntax.
is_error=0
output=""

php_files=""
js_files=""
for f in ${files}
do
  extension=${f##*.}
  case ${extension} in
    php)
      php_files+="${f} "
    ;;
    js)
      js_files+="${f} "
    ;;
  esac
done

if [ -n "${php_files}" ]
then
  output+=`phpcs ${php_files}`
  is_error+=$?
fi

if [ -n "${js_files}" ]
then
  output+=`eslint ${js_files}`
  is_error+=$?
fi

if [ ${is_error} -gt 0 ]
then
  is_error=1
  echo "${output}" | less
else
  is_error=0
fi

exit ${is_error}

これで一応、複数ファイルに対してまとめて構文チェックを行えるようになりました ( 上のスクリプトは構文チェックコマンド入っていないと動かないですが )。出力が流れてしまうので、構文エラーに関しては less に渡して表示するようにしています。色々思うところは以下かなと思います。

  • 文字列連結の半角スペースがダサいので配列使うべきでは?
  • 拡張子ごとにほぼ同じ処理を書いているので、新しい構文チェック追加時に変更大きいよ?

本当は、構文チェックライブラリに変更対象ファイル全部渡して、そっちでフィルタリングしてほしいという気持ちなので今の所シンプルすぎるくらいがちょうどいいのかなと思います。

phpcs, eslint の設定

phpcs

開発環境の依存パッケージに PHPCodeSniffer を追加します。

composer require --dev 'squizlabs/php_codesniffer'

phpcs./vendor/bin/phpcs に入っているのでスクリプトからはそれを読むことにします。

phpcs は実行時に --standard= オプションで使用するルールや設定ファイルを指定することができます。そのほかにも設定ファイルを phpcs.xml という名前で作成しておけば自動的にそのファイルを読んでくれるようです。(自分ディレクトリから親のディレクトリを順に登って行って phpcs.xml を見つけたら使用してくれるみたいです)

PSR2 を継承しつつ色々調整できるように雛形 phpcs.xml を作成してみました。

<?xml version="1.0"?>
<ruleset name="MyRule">
  <description>Coding standard based on PSR2 with some additions for my project.</description>
    <!--
      You can add your rules below.
      For example, you can include new standard, like that;
      <rule ref="PEAR" />

      If you want to know more about phpcs,
      See: https://github.com/squizlabs/PHP_CodeSniffer/wiki
      See also to know the notation of this file: https://github.com/squizlabs/PHP_CodeSniffer/wiki/Annotated-ruleset.xml
    -->

  <!-- Include the whole PSR2 standard -->
  <rule ref="PSR2">
    <!--
      You can exclude specific standard by adding exclude_tag and name_attribute here;
      <exclude name="PSR1.Classes.ClassDeclaration.MissingNamespace" />
    -->
  </rule>
</ruleset>

次は .git/hooks/pre-commit で、ローカルのコマンドと、設定ファイルのパスを使うようにスクリプトを修正します。(設定ファイルは存在チェックだけ必要としています)

app_root_path=`git rev-parse --git-dir`/..
phpcs=${app_root_path}/vendor/bin/phpcs
phpcs_config=${app_root_path}/phpcs.xml

## Execute check.
### PHP
if [ -n "${php_files}" ]
then
  # Check installation of phpcs.
  if [ -e "${phpcs}" ]
  then
    # Check existence of cording standard file.
    if [ -e "${phpcs_config}" ]
    then
      # For upgrading rule file, add -s option to display rule's name.
      output+=`${phpcs} -s ${php_files}`
      is_error+=$?
    else
      output+='\nNOTE: phpcs configuration file is not found. going to check based on psr-2'
      output+='\n'
      output+=`${phpcs} --standard=PSR2 ${php_files}`
      is_error+=$?
    fi
  else
    output+='\nNOTE: phpcs is not installed. php syntax checking is skipped.'
  fi
fi

うん、すごく読みにくい気がします。他の構文チェックする前にシェルスクリプトの構文チェックしろよ的なブーメランですね。

phpcs.xml が見つかった時だけ -s オプションを渡してルール名を表示しておいてあげます。ルールファイルの修正の障壁にならないためにも。

ファイルがないときは何もしない、コマンドがないときはお知らせ、設定ファイルあるときはそれで実行、ないときはPSR2で実行という流れです。

eslint

急に疲れてきてしまったのですが、だいたい phpcs と同じになります。こちらのインストールは yarn add --dev eslintnpm install --dev eslint で行います。

開発環境の依存性でインストールされた後は、初期の雛形ファイルを作成します。 Use popular style guide などで適当に js ファイルとして作っておきました。(ファイル形式も pre-commit でみなきゃいけないかもしれないけど今は js 決め打ちで行きます)

$ yarn add --dev eslint
$ ./node_modules/.bin/eslint --init
$ ls .eslint.js
.eslint.js
$ yarn # init 後にもう一回パッケージのダウンロードが必要...

先ほど同様に .git/hooks/pre-commit を修正します。変更内容みたい方いましたら github でご確認ください。 pre-commit (すみません疲れてしまいました)

まとめ

shell じゃなくて php か js で書けばよかったのではと今更ながら思います。特に制御構文の省略記法とかどれを使っていいか悩んでしまい、ものすごくベタに書きました。それこそ構文チェックが必要な気がします。

まあ、ないよりはマシか、、、という気持ちで書きました。最低限動いたら後は使いながら修正していけばいい、という言い訳をしておきます。

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

開発環境

2019/01/22 追記 Laradock が楽で良いです。

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

【初めてプラグイン作って見た】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

Apollo Client 2.0 with React関連のライブラリ整理

Apollo Client 2.0 with React関連のライブラリをざっくり整理して見ます。失敗したらごめんなさい。詳細は Apollo Docs | Apollo Docs に書いてあります。

目次

Apollo Client

サーバーとの間で、クエリ発行、データ取得、キャッシングなどしてくれます。2017/10にバージョン2.0がリリースされて、大幅にコードの分離が行われました。キャッシング、サーバーとの通信部分が分離されたため、apollo-client-presetを使用しない場合は明示的に指定する必要があります。React, Angular, Vue, その他多くのJavaScriptFrameworkで、GraphQLクライアントとして使用できるそうです。

使い方はインスタンス化したのち、呼び出したいクエリを渡して実行してもらいます。 React の場合は react-apollo から提供されているコンポーネントインスタンスを渡し、子コンポーネントとクエリをHOCに入れてクエリ発行したりします(後述)。

import { ApolloClient } from 'apollo-client';
import { InMemoryCache } from 'apollo-cache-inmemory';
import { HttpLink } from 'apollo-link-http';

const client = new ApolloClient({
  cache: new InMemoryCache(),
  link: new HttpLink({ uri: 'http://localhost:3000' })
});


// クエリの発行
import gql from 'graphql-tag';

client.query({
  query: gql`
    query {
      channels {
        id
        name
      }
    }
  `,
})
  .then(data => console.log(data))
  .catch(error => console.error(error));

Cache関連

apollo-cache-inmemory

Apollo Clientで使用する標準的なインメモリーキャッシュのライブラリ。分離されました。パッケージとしてはApollo Clientと分かれていますが、ソースの管理はapollo-clientリポジトリで行われているのでそこに行くと読めます。

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

Link関連

ネットワークを柔軟に選択することができます。HTTP通信はapollo-link-httpで行えます。

apollo-linkApollo Linkを継承すれば自分でもネットワークを作成することができます。また、httpwsを場合によって切り分けて通信したい場合に、ApolloLink.splitを使って新たなLinkを記述できます。

import { getOperationAST } from 'graphql';

import { WebSocketLink } from 'apollo-link-ws';
import { HttpLink } from "apollo-link-http";
import { ApolloLink } from 'apollo-link';

import { SubscriptionClient } from 'subscriptions-transport-ws';

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,
);

subscriptions-transport-ws

WebSocketのGraphQLエンドポイントに接続するクライアント SubscriptionClient が入っています。

GraphQL関連

graphql-tag

このライブラリから、GraphQLクエリ文字列を、クエリの構文木(GraphQL.js AST format)に変換するgqlテンプレートタグが使用できます。

import gql from 'graphql-tag';

...

export const channelsListQuery = gql`
  query ChannelsListQuery {
    channels {
      id
      name
    }
  }
`;

GraphQL.js

Facebook製のライブラリで、Queryに対してより詳細な処理をしたい時に使ったりします。上の方のLink関連に出てきている、Query内容を調べて処理を分けるといった時に、このライブラリのgetOperationASTを使ってASTから値を読んでいます。

graphql-tools

サーバー側でschemaとresolverを結びつける関数を提供してくれます(schemaは型やクエリ、ミューテーションの仕様を書くもの、resolverは各仕様の実装を書くものです)。

import { makeExecutableSchema } from 'graphql-tools';

const executableSchema = makeExecutableSchema({
  typeDefs,
  resolvers,
});

React関連

react-apollo

Apollo Client向けのReactライブラリです。ApolloProviderコンポーネントgraphql高階コンポーネントを提供してくれます。ApolloProviderはrootコンポーネントをラップして使用し、Apollo Clientのインスタンスをpropに流してくれます。graphql高階コンポーネントはクエリとオプションとコンポーネントを引数に、新しいコンポーネントを生成します。ラップされたコンポーネントpropsdataというプロパティが追加されます。(クエリがMutationのときはmutate?)

graphql(gql`
  {  query... }
`, { options... }
)(Component)

const Component = ({ data }) => (...);

まとめ

書いているうちに未整理なことに気がついてしまいました。GraphQLの話をしているときは、schema、resolverって使っていいのか心配です。Apolloだけだったりする?Relayは触っていないです。。。

読み物

人にオススメ読み物を勧められるように、読み物のリストをまとめておきます。知人アフィリエイトです。地獄のような人間関係です。

不完全にしておよそ正しくないプログラミング言語小史

不完全にしておよそ正しくないプログラミング言語小史

とても好きです

Fine Software Writings

Fine Software Writings

モチベーションを上げたい時に読みます

Joel on Software

Joel on Software -

他の開発者の頭の中が知れていいです

リーダブルコード

リーダブルコード ―より良いコードを書くためのシンプルで実践的なテクニック (Theory in practice)

リーダブルコード ―より良いコードを書くためのシンプルで実践的なテクニック (Theory in practice)

綺麗なコードってなんだっけ、という時に読みます

情熱プログラマー

情熱プログラマー ソフトウェア開発者の幸せな生き方

情熱プログラマー ソフトウェア開発者の幸せな生き方

人生について悩んだ時に読みます

SOFT SKILLS

SOFT SKILLS ソフトウェア開発者の人生マニュアル

SOFT SKILLS ソフトウェア開発者の人生マニュアル

人生について悩んだ時に読みます

ハッカーと画家

Hackers and Painters

やる気出したい時に読みます

リーン・スタートアップ

リーン・スタートアップ

リーン・スタートアップ

サービス作りたい時に読みます

ノンデザイナーズ・デザインブック

自分が作ったページがダサい時に読みます

ノンデザイナーズ・デザインブック [第4版](リフロー版) で読みましたが、リフローだと画面崩れて見難かったです。(リフローの意味を履き違えていなければ、通常版だと画面崩れないはず)

徳丸本

セキュリティ関連で忘れていることがあった時に読みます

徳丸浩のWebセキュリティ教室(日経BP Next ICT選書) も気になりますが読んでないです

サーバー/インフラを支える技術

[24時間365日] サーバ/インフラを支える技術 ?スケーラビリティ、ハイパフォーマンス、省力運用 (WEB+DB PRESS plusシリーズ)

[24時間365日] サーバ/インフラを支える技術 ?スケーラビリティ、ハイパフォーマンス、省力運用 (WEB+DB PRESS plusシリーズ)

なんかサーバーってたくさんあって大変そうだよねという時に読みます

まとめ

随時更新していきます。