静的 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 を使用するようになっているけど、Procfile
に web: 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_at
の range
で切って見ます。
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)
パーティションの切り替え
以下が概要になります。
- 普通のテーブルとして
locations_20181011
を作成します。 - 現在のパーティション
partitions.locations_20181011
をlocations
から Detach します - 新しいパーティションとして
locations_20181011
をlocations
に Attach します - 必要なくなった
partitions.locations_20181011
を削除します - (跡片付け) 新しいパーティションとして Attach した
locations_20181011
をpartitions.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;
以上です。
感想
- テーブルロック怖い
- トリガー怖い
- インデックスも適切に
iOS 版 Chrome の <input type="date">
解決策とかわかったら書きますが、なんなんでしょうかこれ?
iOS 版 Chrome の input type="date" 入力時、何かの条件を満たすと Picker から「消去」が消える
ScreenRecording 07 03 2018 01 51 23
- iOS 11.4
- Google Chrome: 67.0.3396.87 (Official Build) stable (64 ビット)
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_METHODS
でccall
を指定します。 - 呼び出せる関数を
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 でアクセスできるようになます。
感想
また何かに入門だけしているやつです
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 の画面に ws + vnc で繋がった!目標は布団に入ったままプログラミングしたい。(VR 時の写真も取れるようにしたい) pic.twitter.com/RqVFhqEkmo
— ushumpei (@ushumpei_) 2018年5月25日
(これ以降進展なし)
なんで作ってるんですか?
Oculus Go で使える Mac 用のリモートデスクトップアプリが見つからなかったためです (有料のやつある?)。ブラウザで画面共有できる Web サービスもあるのですが、仕事のコードとか書くことを考えるとローカルネットワークで完結するのが気分的に一番良いんじゃないか?と思ったため作り始めました。(自作すること自体はそれはそれでリスクですが)
Mac にはデフォルトで「vnc」と呼ばれる (?) リモートデスクトップのサーバーが入っているため、それ用にクライアントのコード書いたら良いんじゃないか、ということで vnc 周りを調べつつ作っています。
なんで React 360 なんですか?
- Android studio で Android Mobile SDK: C++ 混じってて読めなかった
- Unity: 入ってるけど使ったことない
- React 360: 既存の知識使ってできる
という消極的な理由からです。 1 週間くらいで終わらせたかったのでがっつり学習する必要があるものは避けました(終わってないけど)。今考えると Unity は空間すでにあるし知見も多いため一番良い気がしてます。
どんな感じで作りますか?
という構成を元に、
- 同じローカルネットワーク内のブラウザから WebSocket で Mac の vnc サーバーと接続
- vnc サーバーから送られてくる画面データを React 360 内の canvas に描画
- ブラウザのキー入力やポインター移動イベントをサーバーに通知する
していくように作っています。
できたこと
- React 360 の平面オブジェクト (Plane) に canvas を貼り付ける
- RFB 3.8 プロトコル (vnc のプロトコル) を実装してサーバーとの接続を確立する
- ブラウザでサーバーから画面データを受信する
- 受信したデータを canvas に描画する
できてないこと
- ブラウザのイベントをサーバーに通知する
- ブラウザ -> サーバー への通信は、初めはいらないと思っていたんですがユーザーのサインインが必要なので、ないとログイン画面を延々とみていなければならなくなります
- 画面が全更新になっているので差分更新で済むようにしたい
どうします?
- RFB プロトコルのコードを整理する
- React 360 を一旦やめる
一応リポジトリ: GitHub - ushumpei/VncClient: Vnc client for browser
感想
遊んでないで仕事探す
既存の Laravel プロジェクトに自動構文チェックを軽く入れる
Laravel 始めました。今回はコードの自動構文チェックをすごく軽く入れる話。ゆくゆくは CI に含める予定だけどまだその時間と根回しがないので簡単にやります。
どんな効果を狙っているか?
- レビューにおいて構文に関する指摘が大半を占めているのでその時間を削減する
- 構文に関する規約が暗黙知化しているので (レビューで指摘が多く上がる原因のひとつ)、規約を管理できる構成にする
- 書くのが面白そう!
どんなものを作るか?
- git のプレコミットフック (
git/hooks/pre-commit
) で追加/変更したコードの構文チェックをして、違反していたらコミットできなくする (コミットできない、はやりすぎか?) - 違反している箇所については構文チェックライブラリのエラーを出す (変更行だけチェックできないか? -> 今回はできなかった...)
- 緊急時のためにフックを無視する手段も作っておく(すでにプレコミットフックとして仕組みがある? ->
--no-verify
option) - プレコミットフックがない環境に影響を与えない (まあ多分 pre-commit 配置しなければ良いだけと思う)
- 構文チェックのルールはファイルで管理できるようにする (構文チェックライブラリが標準で備えているはず)
- とりあえずチェック対象は、
js
とphp
(当面は拡張子によって判定)
準備
使うものとしては以下を考えています
- 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 eslint
や npm 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 if
、elseif
共に可ということ。ブラケットではなくコロンを使った記法も可能でその場合はelseif
のみ。コロンの時はendif
を書く。while
、for
、foreach
、switch
もコロンによる記法に関して同様だが、switch
はブロック<?php ... ?>
を分割した時にcase
のインデントでエラーが出ることがある。また、switch
は==
での比較を行う。break
は制御構文の終了、continue
はその回のループを終了。declare
はディレクティブの宣言で、タイプヒントの厳密型チェックとか有効にできる。- ファイル読み込みに関して、
require
はinclude
と違ってエラーを投げる。_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 の依存性がなくなったため必要なら自分で入れてくださいというメッセージが表示されました、という解釈であっている?)。最近の mac は php 入っていると思うので気にしないで大丈夫そうです。(問題あったら是非教えてください)
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
結構重いですね、このコマンド、Rails の rails 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 でサーバーが立ち上がりました!
開発環境
2019/01/22 追記 Laradock が楽で良いです。
結論としてはビルトインサーバーと、 sqlite で頑張ることにします。
$ touch database/database.sqlite $ vim .env # 以下の内容に変更 DB_CONNECTION=sqlite DB_DATABASE=database/database.sqlite
以下ざっとみた感じです。
Homestead: 開発環境としては、サーバーとかデータベースとか全部入りの Homestead という選択肢もあるそうです。 ただ Homestead は Vagrant と VirtualBox (またはその他仮想化ツール) が必要らしいので、若干面倒くさい気分になりました。
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 にたくさん上がっているようです。どこで探すのが正解かわかってないですが、ググると以下のサイトで検索できそうです。
- https://packagist.org/: Composer のパッケージリスト
- https://packalyst.com/: Laravel のパッケージリスト
- https://www.larablocks.com/: Laravel のパッケージリスト
感想
印象としてはフレームワークとしての自由度が、構造を容易に変えられるという意味で、めちゃくちゃ高いなと思いました。でも頑張ればしっかり管理できそうな感じです。バージョン間での差異が少し気になるところですが、逆にそれも自由度が高いが故の変更なのかと思います。どちらかといえばフレームワークの使い方を学ぶよりは、サービスコンテナ自体をしっかりコードを追うなり勉強しておけば、バージョン間の差異についていけるような気がします。
Ethereumの勉強: Hello, World! Contract by Solidity (macOS)
Hello, world!してみようと思います。
以下のリンクを参考にしました。
- Ethereum スマートコントラクト入門:geth のインストールから Hello World まで: 大変わかりやすく、チュートリアルを求めて私の記事を読もうと思っている方は、こちらの記事を読めばいいと思います。
- Ethereum入門: メンテがされていないのか少し古く、solidityのインストールでハマりました。基本概念や、プライベートネットワークの作り方が書いてあります。
- Wiki note on removed geth support for solidity compilation · Issue #14850 · ethereum/go-ethereum: geth で solidity を使うための方法が書かれたissue
- 追記: 2018/04/25: CryptoZombies とても勉強になります!
動機
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"]
とりあえず今のところやることはないですが、eth
、miner
、web3
などをいじって遊んでみるといいと思います。
Install solidity (solc コマンド)
コントラクトを書くために solidity をインストールします。
$ brew install solidity $ brew link solidity $ solc --version
結構時間がかかりました。適当なコマンドを打ってみて、ちゃんとインストールされていることが確認できました。
コントラクトの作成
Hello コントラクトを作成していきます。Hello, world! までの流れとしては以下の 3steps です;
- ソースをコンパイルして abi (Application Binary Interface), bin (Binary)を取得
- 対話環境で
eth.contract
にabi
を食わせてコントラクトの雛形を作成、コントラクトの雛形にコンストラクタ引数として bin と 送信者 と gas を与えてインスタンス化する - インスタンスに対してメソッドの呼び出し(今回は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!"
ようやく出ました!
まとめ
感想
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を意図的に間違えて使ってみる
本題です。以下のようにコードを変更しました。自分としてはsay
とaddMessage
で追加した文章が上から順々にコンソールに表示されればいいなーと思ったのですが全然ダメでした。
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')
も表示されないのはかなり混乱しました。say
とaddMessage
の違いはThreadを指定しないことだけだということで、てっきりsay
はいつでも表示されると思っていたからです。このメッセージはdefaulに追加されているので、Conversationのコールバックが終わった後の最終的な位置は「3」Threadのため表示されないということでした。つまり Conversationのコールバックが終わった時に位置している最終的なThreadの内容がユーザーに返される と考えることができるのではないでしょうか?
また、チャットボットの特性かもしれませんが、だいたいの処理がユーザー駆動になっています。ユーザーの入力があったときにcontrollerでメッセージを受け取るとコールバックが起動されて、各Threadにメッセージが追加されます。あとでわかったことですが Threadに追加したメッセージは原則変更できない ということも意外と重要なことかもしれません。ask
、addQuestion
など、Conversation内でユーザーからの入力を再度受け付けるためのメソッドが存在しますが、この 入力結果を使用して動的にメッセージを生成するにはask
、addQuestion
のコールバック内で呼び出す必要がある ことに注意しなければいけないと思います。(特に{{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
は基本的にask
かaddQuestion
内のコールバックでしか使わない。
まとめ
- 一度にユーザーに返せるのは1つのThread内のメッセージ
- Threadはネストさせることはできない
- Conversationのコールバックが終わった時に位置している最終的なThreadの内容がユーザーに返される
- Threadに追加したメッセージは原則変更できない
- 入力結果を使用して動的にメッセージを生成するには
ask
、addQuestion
のコールバック内で呼び出す必要がある - Threadは再利用できる形で作っておく
convo.gotoThread
は基本的にask
かaddQuestion
内のコールバックでしか使わない。
感想
- めちゃくちゃ煩雑で雑多な内容になりました。
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自体に関しては以下のリンクで勉強しました。
- アプリ開発の流れを変える「GraphQL」はRESTとどう違うのか比較してみた
- 自分のやつ: GraphQLに関するメモ
- GraphQL入門 - 使いたくなるGraphQL
- ReactとApolloを使ってGithub GraphQL APIを試してみる
目次
Apolloプロジェクトの概要
ApolloはMeteor 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も使いません。よろしくお願い申し上げます。
完成品のソースです。
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 gql
、query
定数と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
を開いてみてください。左のペインで以下のクエリを実行すると、右に実行結果(モックデータ)が表示されます。
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 を削除し、 makeExecutableSchema に resolvers を渡します。
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, );
という感じです。
自作Vimプラグインの置き場所と置き方
先日プラグインを作成してみた記事を公開しましたが、その記事のコメントで tyruさん からご指摘いただいた(とてもありがたいです)、プラグインのインストール場所と方法について整理しておきます。
分類
そもそも自分で書いたVim スクリプトをVimに読んでもらうためには次の3つが考えられると思います。
ここではデバッグのことを考えて自力で配置する方法「1. プラグインとして配置する」「2. パッケージとして配置する」に限定して説明していきます。なので、「3. プラグインマネージャーを使用して配置する」に関しては書きません(プラグインマネージャーのリファレンスを読んだ方がわかると思います。私は dein.vim 使っています)
前提
プラグインのリポジトリは plugin と autoload の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 |
私が作成したプラグインを配布する場合は以下のようになります。
$ 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
プラグイン作成時に plugin や autoload などのディレクトリでコードを分けて作った理由がようやく実感できました。
私が作成したプラグインを配布する場合は以下のようになります。
$ 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勉強しました的な記事を書かせていただきました。自分がプラグインを作って見た時に感じたことがつらつらと書いてあります。
プラグインはVimの version 8.0 で書きました。もし試していただけた方で、動かないよ!ということがあれば、大変恐縮ですがその旨コメントしていただけると幸いです。(ごめんなさい!バージョンごとの文法の違いは追えてません)
目次
動機
基本的な操作は覚えたけどもっとVimに関して詳しくなりたいというのが動機です。聞いた話によると、 Vim知るにはVim script書くのが早い ということなので勉強し始めました(すごい人たちは書いているし)。せっかくなのでplugin作ってみようと思い、今回はMarkdownのテーブルを作るプラグインを作成することにしました。同じ機能のプラグインはもうすでにいくつかありますが、世の中ほとんどのものはすでに作られているのでしょうがない、ということで練習のつもりで作りました。
リファレンス
基本的な文法については以下のリンクにお世話になりました。
- Vimスクリプト基礎文法最速マスター: 可変長引数、分割代入素敵
- usr_41 - Vim日本語ドキュメント:
execute
、normal
コマンドとか整理しなければ。range
キーワードとか面白い - モテる男のVim script短期集中講座Add Star: 辞書は関数モテる。try..catch、abortでエラーハンドリング。mapの文法むずい
- usr_41 - Vim日本語ドキュメント | プラグインを書く: mapの文法怖い
- usr_41 - Vim日本語ドキュメント | ライブラリスクリプトを書く: autoloadについて
作るもののイメージ
インタフェースとしては :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: インタフェースを記述しています。上記の関数をコマンド、キーマップとして登録しています。
学んだことの詳細
コマンドの定義と引数の渡し方
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>
を使う意味はユーザーが独自にマッピングするためだとリファレンスにも書いてあるんですけどね!
カレントバッファへの書き込み
これは本当にいい方法だったのかわかっていないのですが、以下のように execute
と normal
を使って記述しました。
" 変数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は途中でエラー出ても止まらない力強いスクリプトなので、間違った入力でもなんらかの出力をしてくれちゃうみたいです。書くにあたってそのあたりのエラーハンドリングのために try と abort を使って見ました。
感想
「Vim少し使える」と思っていたのですが、全然知らないことが多くて驚いています。 プラグインを書いた後に自分の .vimrc
を見返してみると、今まで曖昧で済ましていた部分が違って見えてとても嬉しい です。
僕はもともと、Vim自体の使い勝手を変えないように、プラグインは使わないようにしていたのですが、今回プラグインを書いたことで、各プラグインがどのようにVimに影響を与えているのか少しわかるようになったため、これからは多少使っていってもいいという気持ちになりました。
これからのことで言えば、もう少し実際に役に立つ機能をかいて行きたい感じです。このプラグインで言えば、多分テーブルとか最初からサイズ決めうちより調節機能とかあったほうがいいとか思います。(まあでも、他のプラグインを色々触ってみることから始めます)
拙い文章、内容になりました。 読んでいただいてありがとうございます!!!
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));
- Introduction | Apollo Client
- GitHub - apollographql/apollo-client: A fully-featured, production ready caching GraphQL client for every server or UI framework
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関連
apollo-link, apollo-link-http, apollo-link-ws
ネットワークを柔軟に選択することができます。HTTP通信はapollo-link-http
で行えます。
apollo-link
のApollo Link
を継承すれば自分でもネットワークを作成することができます。また、http
とws
を場合によって切り分けて通信したい場合に、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, );
- GitHub - apollographql/apollo-link: Interface for fetching and modifying control flow of GraphQL requests
- Http Link | Apollo Link
- WebSocket Link | Apollo Link
subscriptions-transport-ws
WebSocketのGraphQLエンドポイントに接続するクライアント SubscriptionClient が入っています。
- GitHub - apollographql/subscriptions-transport-ws: A WebSocket client + server for GraphQL subscriptions
- Apollo Docs | Apollo Docs
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, });
- GitHub - apollographql/graphql-tools: Build and mock your GraphQL.js schema using the schema language
- Resolvers | GraphQL Toolst
React関連
react-apollo
Apollo Client向けのReactライブラリです。ApolloProvider
コンポーネントとgraphql
高階コンポーネントを提供してくれます。ApolloProvider
はrootコンポーネントをラップして使用し、Apollo Clientのインスタンスをpropに流してくれます。graphql
高階コンポーネントはクエリとオプションとコンポーネントを引数に、新しいコンポーネントを生成します。ラップされたコンポーネントはprops
にdata
というプロパティが追加されます。(クエリがMutationのときはmutate
?)
graphql(gql` { query... } `, { options... } )(Component) const Component = ({ data }) => (...);
まとめ
書いているうちに未整理なことに気がついてしまいました。GraphQLの話をしているときは、schema、resolverって使っていいのか心配です。Apolloだけだったりする?Relayは触っていないです。。。
読み物
人にオススメ読み物を勧められるように、読み物のリストをまとめておきます。知人アフィリエイトです。地獄のような人間関係です。
不完全にしておよそ正しくないプログラミング言語小史
とても好きです
Fine Software Writings
モチベーションを上げたい時に読みます
Joel on Software
他の開発者の頭の中が知れていいです
リーダブルコード
リーダブルコード ―より良いコードを書くためのシンプルで実践的なテクニック (Theory in practice)
- 作者: Dustin Boswell,Trevor Foucher,須藤功平,角征典
- 出版社/メーカー: オライリージャパン
- 発売日: 2012/06/23
- メディア: 単行本(ソフトカバー)
- 購入: 68人 クリック: 1,802回
- この商品を含むブログ (138件) を見る
綺麗なコードってなんだっけ、という時に読みます
情熱プログラマー
- 作者: ChadFowler,でびあんぐる
- 出版社/メーカー: オーム社
- 発売日: 2017/07/15
- メディア: Kindle版
- この商品を含むブログを見る
人生について悩んだ時に読みます
SOFT SKILLS
- 作者: ジョン・ソンメズ
- 出版社/メーカー: 日経BP社
- 発売日: 2016/06/02
- メディア: Kindle版
- この商品を含むブログ (6件) を見る
人生について悩んだ時に読みます
ハッカーと画家
やる気出したい時に読みます
リーン・スタートアップ
- 作者: エリック・リース,伊藤穣一(MITメディアラボ所長),井口耕二
- 出版社/メーカー: 日経BP社
- 発売日: 2012/04/12
- メディア: 単行本
- 購入: 24人 クリック: 360回
- この商品を含むブログ (95件) を見る
サービス作りたい時に読みます
ノンデザイナーズ・デザインブック
- 作者: Robin Williams
- 出版社/メーカー: マイナビ出版
- 発売日: 2016/09/20
- メディア: Kindle版
- この商品を含むブログを見る
自分が作ったページがダサい時に読みます
ノンデザイナーズ・デザインブック [第4版](リフロー版) で読みましたが、リフローだと画面崩れて見難かったです。(リフローの意味を履き違えていなければ、通常版だと画面崩れないはず)
徳丸本
体系的に学ぶ 安全なWebアプリケーションの作り方[固定版] 脆弱性が生まれる原理と対策の実践
- 作者: 徳丸浩
- 出版社/メーカー: SBクリエイティブ
- 発売日: 2013/08/14
- メディア: Kindle版
- この商品を含むブログを見る
セキュリティ関連で忘れていることがあった時に読みます
徳丸浩のWebセキュリティ教室(日経BP Next ICT選書) も気になりますが読んでないです
サーバー/インフラを支える技術
[24時間365日] サーバ/インフラを支える技術 ?スケーラビリティ、ハイパフォーマンス、省力運用 (WEB+DB PRESS plusシリーズ)
- 作者: 安井真伸,横川和哉,ひろせまさあき,伊藤直也,田中慎司,勝見祐己
- 出版社/メーカー: 技術評論社
- 発売日: 2008/08/07
- メディア: 単行本(ソフトカバー)
- 購入: 133人 クリック: 2,270回
- この商品を含むブログ (288件) を見る
なんかサーバーってたくさんあって大変そうだよねという時に読みます
まとめ
随時更新していきます。