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件) を見る
なんかサーバーってたくさんあって大変そうだよねという時に読みます
まとめ
随時更新していきます。
CLIの豆知識
友人に教える必要があったのでメモします。
概念
- CLI(Command Line Interface) プログラマーとかのラップトップをのぞくと画面に表示されている黒い背景のウィンドウがそれです。ユーザーがコンピューターに何かさせたい時に、コマンドを実行できるインターフェースのことです
- GUI(Graphical User Interface) はCLIじゃないやつ。コマンドを使用しないで視覚的にマウスだったり指だったりで操作するインターフェースです。ブラウザとかエクセルとかGUIアプリケーションです。
- Terminal.app はMacに標準でインストールされているCLIアプリケーション。Applications > Utilities > Terminal.app から開けます。
コマンド
主要なコマンドは以下の通りです。コマンドの実行はコマンド入力後 Enter キーを押してください。入力途中で tab キーを押すと文字列を保管してくれて便利です。
pwd
: 現在のディレクトリの位置を確認ls
: 現在のディレクトリに含まれているファイルの一覧を表示cd ディレクトリ名
: ディレクトリの移動exit
: ターミナルを閉じますopen ファイル名
: ファイルを開きます。GUIで操作したくなったらこれを使う感覚です。
ls
に関するTips
ls -l
: ファイル一覧の詳細表示ls -la
: 隠しファイルも含めたファイル一覧の詳細表示
cd
に関するTips
open
に関するTips
open .
: 現在いるディレクトリをファインダーで開く
カーソル
ターミナルではマウスによる操作が制限されているため、カーソルの移動ショートカットを覚えると便利です。ターミナルだけでなく色々な場面でこのショートカットは使えるので生産性が向上します。(emacs
キーバインドと言います。ほとんどのアプリケーションの入力箇所でこのショートカットは有効です)
Ctrl + f
: 行末方向へ移動Ctrl + b
: 行頭方向へ移動Ctrl + e
: 行末へ移動Ctrl + a
: 行頭へ移動Ctrl + d
: カーソル位置の文字を削除Ctrl + h
: カーソルのひとつ前の文字を削除Ctrl + k
: カーソル位置の文字から行末までの文字を全て削除Ctrl + u
: カーソル位置の文字から行頭までの文字を全て削除
入力ミスして一旦全てのコマンドを消したい時には、Ctrl + u
または Ctrl + a
+ Ctrl + k
を使ったりします。(Ctrl + c
でも同じことができますが、これはコマンドキャンセルのショートカットなので、手癖にならないようにしてます)
履歴
実行したコマンドは全てではないですが履歴に残ります。履歴を使うことで打ち間違えなどのミスを減らすことができます。保持されている全ての履歴を表示するコマンドはhistory
です。
Ctrl + p
: ひとつ前に入力したコマンドを表示します(連続して入力することでどんどん遡っていくことができます)Ctrl + n
: ひとつ後に入力したコマンドを表示します(遡りすぎた時に戻るために使います)Ctrl + r
: 履歴検索対話プログラムが起動し、入力した文字に部分マッチする直近の履歴を表示します。結果が正しくて実行したい場合はEnter
、正しいけど実行したくはない場合はCtrl + a
など何か他のショートカット、正しくない場合はCtrl + r
でさらに遡る、もうやめたい場合はCtrl + c
でキャンセルします。
まとめ
快適に使うための豆知識をまとめて見ました。多分知ってるよとか、これ足りないとかあると思いますが、とにかく言いたいことはemacs
キーバインドがものすごく便利だということです。僕はプログラミング始める時にvim
かemacs
で迷って結局vim
にしたのですが、その時に軽くemacs
触っていたおかげで、たくさんのアプリケーションで快適な入力体験ができています。
あとCLIのショートカットとか便利な使い方は友達とか先輩から教えてもらったことが多いので、自分も誰かに教えたくなったというのも動機です。
GraphQLに関するメモ
GraphQLについて調べたことのメモです。
GraphQLはFacebookによって考案・開発されたクライアントアプリ向けのクエリ記述言語です(仕様)。モバイルアプリ向けの使いやすいAPIを実現するために考えられていて、2015年にオープンソースになって騒がれ始めました。Facebook, GitHub, Pinterest…など大規模な方々が使っているそうです。
思想としてはRESTにかけている柔軟性をAPIに持たせるために作られていて、 単一のエンドポイントから必要なデータを必要な分、明示的に取ってくることができるようになっています。うまくやればデータ通信量を減らせる上にいろいろメリットがあったりします。
概要
仕様によるとGraphQL以下のコンセプトでデザインされているそうです。私の意訳、ひどいので感じを掴むくらいで読んでください。また何かいいものがあれば教えていただきたいです。
- 階層型データ構造(Hierarchical):
- クエリ自体が階層を持っているので、レスポンスと構造が一致して理解しやすいし、検索条件なども記述しやすい。
- 製品中心(Product‐centric):
- フロントエンドとフロントエンドエンジニアファースト。
- 強い型付け(Strong-typing)
- クエリが定義された型によって安全に実行されることが保障されます。
- クライアント主導の問い合わせ(Client‐specified queries)
- 従来のAPIはサーバー側が何を送るか決めていましたが、クライアント側がフィールド単位で何が欲しいかで問い合わせることができます。
- 内省的(Introspective)
- GraphQLサーバーはGraphQLで問い合わせできるようになっている(サーバーに型定義を問い合わせることができるので、その意味かと思います?)
GraphQLのクエリとレスポンスのイメージ
例えばブログサイトのAPIとかでユーザーの記事のタイトルを一覧表示しようと思った場合、RESTだとユーザーに紐付く記事一覧をGETするエンドポイント(/users/:id/posts
とか、または/posts?user=id
)を作って、そこを叩いて返ってきたレスポンスをクライアント側でフィルタリングするかと思いますが、GraphQLだと単一のエンドポイントで記事名の一覧のみを取得できるクエリがかけます。
query { user(id: "1") { name posts { title } } }
上のクエリをクライアントから発行すると、クエリとそっくりな構造の以下のレスポンスが返ってきます。
{ "data": { "user": { "name": "ushumpei", "posts": [ { title: "GraphQLの概要" }, { title: "Relayで遊ぶ" } ] } } }
キーワードの説明
サーバーにスキーマと呼ばれる形で、API定義を行います。以下のキーワードを用いて定義していきます。
- 型定義
- Type
- Interface
- 操作定義
- Query
- Mutation
- Fragment (補助)
- Input (補助)
Type
型の定義を行います。
例
type User { id: String name: String posts: [Post] ... }
Interface
型から継承できるインタフェースも定義できます。フィールドとしてインタフェースの型を指定するような、以下のような使い方ができます。
例
interface Media { url: String } type Image impliments Media { url: String width: String ... } type Audio impliments Media { url: String playTime: Int ... } type Post { title: String media: Media // Image, Audio両方をフィールドに持てる。この場合インタフェースで定義されているurlのみ取得可能。 ... }
Query
受け付けるクエリを定義します。
type Query { user(id: String): User users: [User] }
この定義に対しては以下のようなクエリをクライアントから発行できるイメージです。Userのどのフィールドを取ってくるとかは自由です。
query { user(id: "1") { id name profile } }
Mutation
更新を受け付けるためのインタフェースを定義します。
type Mutation { createUserPost(userId: String!, title: String!, body: String!): Post }
クライアントからは以下のような問い合わせが想定できます。
mutation { createUserPost(userId: "1", title: "GraphQLの概要", body: "GraphQLについて...") { id title } }
(補助)Fragment
クライアント側からレスポンスを記述しやすくするために、複数のフィールドをひとかたまりに断片化してQuery、Mutationに記述できます。
fragment basicInfo on User { id name profile } query { user: { ...basicInfo } }
(補助)Input
Query、Mutationに対して引数のセットを定義できます。先ほどのMutationは次のように書き直せます。(作成、更新とかで引数をまとめられるのと、type Mutation
が読みやすくなると思います)
input PostInput { userId: String! title: String! body: String! } type Mutation { createUserPost(post: PostInput!): Post }
とりあえず試すには?
チュートリアルを公開してくださっている記事があるのでやって見たら少し解りました。 ただしApolloClientのバージョンが2になりnetworkInterface
、customResolvers
など消えた機能があるのでバージョン気をつけてください、バージョン
参考: All you need to know about Apollo Client 2
問題
あくまでクエリ記述言語なので、どのようにデータを取得するかは自分で書く必要があります。データストレージとして何を採用するかなども考える余地しかない感じです。
とりあえずは、graphql-server-express
(apollo-server-express
に変わった?)とかで練習できますが、データ保持はオンメモリしかやっていないです。
外部ホスティングサービスとしてはざっと検索すると、scaphold.io, graph.cool, meldioとかありますが、まだ使って見ていないのでなんとも言えません。
感想
GraphQL旨味としては クライアントがなんのデータを取得しているか楽に明示できることだと思います。クエリの形を見れば何を取ってきているかわかります。
RESTでも取得するフィールドをクエリパラメータで指定して取ってこれるようにすれば取得しているデータを明示できますが、検索条件とかも色々絡んでくるとクエリ自体がかなり長くなって脳に負担になってきます。
またその場合ドキュメンテーション必須です。それに比べるとGraphQLは共通認識化しやすそうですし、定義したスキーマがドキュメントの役割を果たせるので(実際、型定義の際にdescriptionフィールドにmarkdownでドキュメンテーションできる)そこも楽になるかと思います。
雑記
- ライブラリの名前変わったりいろいろモジュール化が盛んなのでもうちょっと頭整理したい
- GraphQL運用で使って見たスライドとか調べるとある
- データ永続化はPostGraphQLとかどうなんでしょうか
- NoSQLでのうまい使い方とか誰か考え済みなんでしょうか。NoSQLのデータ設計方法が全然わかっていないので一度どこかでちゃんと体験しないといけない。
- 「フィリピン -> ネットインフラ弱い -> でもネット高い -> でもFacebookアクセス無料 -> みんなめっちゃ使う」の裏でGraphQLが活躍しているのかもしれない
Gitについて自分なりに説明してみた
友達に説明する必要があったのでGitの使い方を誤解を恐れずメモしておきます。自分の知識の整理も兼ねています。
この記事ではローカルマシン(一台のPC)での操作で完結しています。リポジトリの作成、コミットの作成、ブランチの作成、マージまでを範囲としています(これらの単語は順を追って説明していけたら、と思います)(ブランチの扱いは、最終的なリポジトリの形としてはgithub-flow
です。)
Gitの概要
Gitは 分散型バージョン管理システム です。というとやばく聞こえますが、Gitを使う目的は 「大勢で1つのアプリケーションを一緒に作ること」 です。なのでそのために必要そうなバージョン管理機能がちゃんと備わっています。これあるべきでしょうとか予想しながら読んでいっていただけるといいかと思います。以下チュートリアルを通して概念と機能を説明していきます。
この記事を読み進めるために
- 環境: Mac (他の環境の場合、概念と機能の説明は問題ありませんが、手順に関して読み替えが必要です)
- 知識: ターミナルの基本的な操作 (自信がなくなった場合、こちらかこちらを適宜参考にしながら読んでいただければ幸いです)
チュートリアルを始める前にちょっとした下準備を行います。
(下準備)gitコマンドがインストールされているか確認
今回は CLI での使い方を説明していきます。他の使い方としては、GUIアプリケーションなど沢山開発されているのでそれ経由で操作するのもありです。(その場合GUIアプリの解説ページに色々書いてあると思うので、この記事は閉じてしまって大丈夫です!)
Macを使っている場合、すでにGitがインストールされているかと思います。アプリケーションフォルダのユーティリティの中にある ターミナル.appを開いて、Gitがインストールされていることを確認してみましょう。ターミナルを開いたらgit --version
と入力して、実行してください。
$ git --version git version 2.13.6 (Apple Git-96)
git version ...
のような表示が出たらインストールされています。
$ git --version -bash: git: command not found
と出たらインストールされていないので、ググってインストールしてください。
(下準備)Gitの初期設定
以下のコマンドを実行して、Gitのユーザー名、アドレスが登録されていることを確認してください。
$ git config user.name ushumpei $ git config user.email mail@ushumpei.com
登録されていない(実行しても何も表示されない)場合は次のコマンドを実行してユーザー情報を登録しておいてください。
$ git config --global user.name "あなたの名前" $ git config --global user.email あなたのアドレス
チュートリアル
リポジトリ作成
Gitによるバージョン管理はファイル単位で行われます。と言ってもコンピューターすべてのファイルを管理するわけではなく、管理する範囲は限定することができます。限定できる範囲はディレクトリ(フォルダ)単位で、Gitによって管理されるのはそのディレクトリに含まれているすべてのファイルになります。この限定された範囲を リポジトリ(repository) と呼びます。
実際にリポジトリを作ってみましょう。デスクトップにgit_tutorial
リポジトリを作ります。
ターミナルで 1.デスクトップに移動
2.git_tutorialディレクトリの作成
3.新規ディレクトリ内へ移動
4.ディレクトリのgitリポジトリ化
を行います。TAB
キーでファイル名を補完したりして頑張ってください。
~$ cd ~/Desktop/ ~/Desktop$ mkdir git_tutorial ~/Desktop$ cd git_tutorial ~/Desktop/git_tutorial$ git init Initialized empty Git repository in /Users/ushumpei/Desktop/git_tutorial/.git/
Initialized empty Git repository in ...
が出たら成功です。リポジトリが完成しました。
注意: もしかすると日本語環境の場合~/Desktop
がないかもしれないです(この辺りは自信ないです)。~/デスクトップ
など試してみてください。
git status
というコマンドを実行して見てください。このコマンドは現在のリポジトリの状態を表示してくれるものです。 とてもよく使います 、他のコマンドを打つ際もこのコマンドで実行前、実行後の変化の確認をしながらやると何をしているかわかってくると思います。
~/Desktop/git_tutorial$ git status On branch master Initial commit nothing to commit (create/copy files and use "git add" to track)
各行の意味は以下です。不明な単語については後ほど説明いたします。
- 現在は
master
という ブランチ(branch)にいる - まだ コミット(commit) が存在しない
- コミットの対象に含まれているものはない
Initial commit
管理下にあるファイルに対して、「誰が、いつ、なぜ、どのファイルの、どこの部分を、どのように変更したか(変更に関する5W1H)」という変更情報を管理するのがGitの大きな役割です。この変更情報は コミット(commit) と呼ばれます。コミットは開発者が好きな粒度で作成することができます、複数ファイルの変更を1つのコミットに含めるのが一般的です。
(コミットのイメージ) 変更理由: モバイルブラウザでトップページの挨拶文の表示が崩れてしまっていたので修正 日付: 2017/11/27 変更者: ushumpei index.htmlというファイルの12行目を`<p>こんにちは</p>`から`<h1>こんにちは</h1>`に変更 stylesheet/mobile.cssというファイルを追加
コミットを作成する手順は次のようになります;
- 変更: リポジトリ内でファイルに関する変更を行う(作成、更新、削除のどれか)
- ステージ:
git add
、git rm
などのコマンドで、どの変更内容を履歴に残したいかGitに知らせる - コミット:
git commit
コマンドで変更履歴を作成する
超個人的なイメージではGit管理下のファイルを変更したら「変更内容の紙が出てくる(変更) -> 変更に含めたい紙を束ねる(ステージング) -> キリのいいところで束を箱に入れる(コミット)」ということをやっている感じです。
では実際にファイルを作り、Gitの管理下に追加してみます。1.ファイルの作成
2.ファイルを追加対象に含める
3.追加
の流れになります。git status
を使いながら1つ1つ変化を追って確認していきましょう。
~/Desktop/git_tutorial$ git status On branch master Initial commit nothing to commit (create/copy files and use "git add" to track)
先ほどと同じ状態です。
~/Desktop/git_tutorial$ touch index.html # index.htmlが作成されたので、好きなエディタで開いて以下の内容を記述して保存してください(# は不要です); # <html> # <head> # <title>git_tutorial</title> # </head> # <body> # <p>Git Tutorial</p> # </body> # </html> ~/Desktop/git_tutorial$ git status On branch master Initial commit Untracked files: (use "git add <file>..." to include in what will be committed) index.html nothing added to commit but untracked files present (use "git add" to track)
git status
によると、依然としてコミットの対象に含まれているものはないですが、新しくindex.html
というファイルが作成されていて、まだGit管理下に入っていないという情報が表示されています。
Git管理下に含めるにはgit add
コマンドを実行してくださいという指示が括弧中に記載されているので実行します。
~/Desktop/git_tutorial$ git add index.html ~/Desktop/git_tutorial$ git status On branch master Initial commit Changes to be committed: (use "git rm --cached <file>..." to unstage) new file: index.html
git status
によると、コミットの対象に「index.htmlを新規作成した」という変更を追加する準備ができたことが表示されています。この状態をindex.html
の変更が ステージ(stage) されていると言い、この変更のステージを取りやめることを アンステージ(unstage) すると言い、git rm --cached index.html
(場合に応じてgit reset HEAD index.html
)でGitに知らせることができます。
最後にステージされている変更からコミットを作成してみましょう。
~/Desktop/git_tutorial$ git commit -m "Initial commit" [master (root-commit) c04dc8f] Initial commit 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 index.html ~/Desktop/git_tutorial$ git status On branch master nothing to commit, working tree clean
上記のように、追加に関する情報が表示されたら成功です。git status
の結果は変更を全く行なっていない状態に戻っているかと思います。
これで 変更->ステージ->コミット の1サイクルが終わりました。Initial commitの部分は変更理由を記述します。慣習としてリポジトリを作成した直後のコミットはInitial commitと書くことが多いです。
作成したコミットを見る: git log
作成したコミットを見て見ましょう。git log
、git log -p
を実行してください。
~/Desktop/git_tutorial$ git log commit c04dc8ffd000dd7ba23b20177c208e9095901d19 (HEAD -> master) Author: ushumpei <shumpei.uzawa@gmail.com> Date: Tue Nov 28 00:59:56 2017 +0800 Initial commit ~/Desktop/git_tutorial (master)$ git log -p commit c04dc8ffd000dd7ba23b20177c208e9095901d19 (HEAD -> master) Author: ushumpei <shumpei.uzawa@gmail.com> Date: Tue Nov 28 00:59:56 2017 +0800 Initial commit diff --git a/index.html b/index.html new file mode 100644 index 0000000..e69de29
上記のコマンドで過去に行なったコミット内容が全て確認できます。変更の詳細がいらない場合はgit log
、詳細を見たい場合はgit log -p
変更履歴が確認できます。
更新
続いてindex.html
を更新してコミットして見ましょう。index.html
をエディタで開いて以下のように修正してください。(6行目のpタグをh1タグに変更します)
... <h1>Git Tutorial</h1> ...
git status
を実行して変化を見てみてください。
~/Desktop/git_tutorial$ git status On branch master Changes not staged for commit: (use "git add <file>..." to update what will be committed) (use "git checkout -- <file>..." to discard changes in working directory) modified: index.html no changes added to commit (use "git add" and/or "git commit -a")
index.html
が変更されたことが表示されています。もしstageしたいなら先ほどと同様にgit add index.html
、変更を破棄して最後のコミットの状態に戻したいならgit checkout index.html
を実行してくださいという指示が書かれています。
ここでどんな変更が行われたかを確認するコマンドgit diff
を使ってみましょう。
~/Desktop/git_tutorial$ git diff diff --git a/index.html b/index.html index 135d8e3..243ca88 100644 --- a/index.html +++ b/index.html @@ -3,6 +3,6 @@ <title>git_tutorial</title> </head> <body> - <p>Git Tutorial</p> + <h1>Git Tutorial</h1> </body> </html>
index.html
に対する変更が表示されています。先ほど行なった通り、pタグの行が削除され(- <p>Git Tutorial</p>
)、h1タグの行が追加され(+ <h1>Git Tutorial</h1>
)ていることがわかります。そのまんまですが、行頭の-
は削除、+
は追加の意味です。(差分は文字単位ではなく、行単位での表示となります)
この変更で問題ないのでステージしてコミットしましょう。
~/Desktop/git_tutorial$ git add index.html ~/Desktop/git_tutorial$ git status On branch master Changes to be committed: (use "git reset HEAD <file>..." to unstage) modified: index.html ~/Desktop/git_tutorial$ git commit -m "ページタイトルなのでh1にタグを変更" [master 081f264] ページタイトルなのでh1にタグを変更 1 file changed, 1 insertion(+), 1 deletion(-) ~/Desktop/git_tutorial$ git status On branch master nothing to commit, working tree clean
1サイクルが終わりました。これまで行なった作業を確認するために、git log --graph
を実行してみてください。2つのコミットが追加されていることが確認できるかと思います。
~/Desktop/git_tutorial$ git log --graph * commit 081f264cc85e5074a93f7304ba2febf9efb90bd9 (HEAD -> master) | Author: ushumpei <shumpei.uzawa@gmail.com> | Date: Tue Nov 28 10:17:57 2017 +0800 | | ページタイトルなのでh1にタグを変更 | * commit b80d4812cbde6655bee82bfa20127519e8826b81 Author: ushumpei <shumpei.uzawa@gmail.com> Date: Tue Nov 28 10:02:38 2017 +0800 Initial commit
--graph
オプションをつけて実行したので、2つのコミットが線で繋がって表示されています。これは ブランチ(branch) と呼ばれる概念に関係しています。
ブランチ(branch)
ブランチ(branch) について説明します。ブランチは連続したコミットの集まりです。先ほど作った2つのコミットはmaster
ブランチの先頭に追加されています、どんどんコミットを重ねてブランチ(枝)を伸ばしていくイメージです。
ブランチと呼ばれるからには 枝分かれ させることができます、ブランチの途中から別の ブランチを生やして そちらを伸ばしていくことができます。この機能により、ブランチを分けることによって複数の開発者が互いに影響を受けずに並列に開発を行っていくことができます。(あとで出てきますが、枝分かれしたブランチは好きなタイミングで他のブランチに合流させることができます)
リポジトリにどんなブランチがあるのかはgit branch
コマンドで確認できます。
~/Desktop/git_tutorial$ git branch * master
Gitリポジトリを作った直後、ブランチはmaster
しかありません。先ほどのindex.html
に関する変更はmaster
に追加していました。master
から違うブランチを生やしてコミットを追加してみましょう。ブランチの作成はgit branch ブランチ名
です。
# 一行目より現在masterブランチにいることがわかります ~/Desktop/git_tutorial$ git status On branch master nothing to commit, working tree clean # stylingブランチを作成 ~/Desktop/git_tutorial$ git branch styling # ブランチ一覧にstylingが表示されています、「*」は現在いるブランチを表しています ~/Desktop/git_tutorial$ git branch * master styling # git checkout ブランチ名でstylingブランチに移動します ~/Desktop/git_tutorial$ git checkout styling Switched to branch 'styling' # 一行目より現在stylingブランチにいることがわかります ~/Desktop/git_tutorial$ git status On branch styling nothing to commit, working tree clean # ブランチ一覧でも同様に確認できます ~/Desktop/git_tutorial$ git branch master * styling
それではstyling
ブランチの先頭にコミットを追加します。スタイルシートファイルstyle.css
を作成して、index.html
でそれを読み込んでスタイルを適用しましょう。
~/Desktop/git_tutorial$ touch style.css # style.cssを好きなエディタで開いて以下の内容を記述して保存してください; # h1 { # color: #727272; # font-family: monospace; # } # # index.htmlは次のようにheadタグ内にlinkタグを追加してください; # <html> # <head> # <title>git_tutorial</title> # + <link rel="stylesheet" href="./style.css" /> # </head> # ... ~/Desktop/git_tutorial$ open index.html # ブラウザで表示してみてもいいかもです # 変更の確認をしましょう。index.htmlが変更され、style.cssが追加されています ~/Desktop/git_tutorial$ git status On branch styling Changes not staged for commit: (use "git add <file>..." to update what will be committed) (use "git checkout -- <file>..." to discard changes in working directory) modified: index.html Untracked files: (use "git add <file>..." to include in what will be committed) style.css no changes added to commit (use "git add" and/or "git commit -a") # index.html、style.cssをステージします ~/Desktop/git_tutorial$ git add . # 2つのファイル変更がステージされています ~/Desktop/git_tutorial$ git status On branch styling Changes to be committed: (use "git reset HEAD <file>..." to unstage) modified: index.html new file: style.css # コミットします。現在stylingブランチにいるのでコミットはstylingブランチの先頭に追加されます(stylingブランチが1つ伸びます) ~/Desktop/git_tutorial$ git commit -m "スタイルシートを追加" [styling 59df46b] スタイルシートを追加 2 files changed, 5 insertions(+) create mode 100644 style.css
コミットが追加されました。ブランチの形を確認してみましょう。
~/Desktop/git_tutorial$ git log --graph * commit 59df46ba1df6e9a29584dd2ad2d95b79248f0409 (HEAD -> styling) | Author: ushumpei <mail@ushumpei.com> | Date: Tue Nov 28 11:10:48 2017 +0800 | | スタイルシートを追加 | * commit 081f264cc85e5074a93f7304ba2febf9efb90bd9 (master) | Author: ushumpei <mail@ushumpei.com> | Date: Tue Nov 28 10:17:57 2017 +0800 | | ページタイトルなのでh1にタグを変更 | * commit b80d4812cbde6655bee82bfa20127519e8826b81 Author: ushumpei <mail@ushumpei.com> Date: Tue Nov 28 10:02:38 2017 +0800 Initial commit
3つめのコミットが今追加したものです。今は直線で表示されているので枝分かれ感が薄いですが、2つめのコミットからstylingブランチがmasterブランチから分かれて、1コミット分伸びたという情報が表示されています。このコミットの変更はmasterブランチには影響しません。そのことをmasterブランチに移動して確認してみましょう。ブランチの移動はgit checkout ブランチ名
です。
# masterブランチへ移動 ~/Desktop/git_tutorial$ git checkout master Switched to branch 'master' # 状態の確認 ~/Desktop/git_tutorial$ git status On branch master nothing to commit, working tree clean # ファイルの確認 ~/Desktop/git_tutorial$ ls index.html
先ほど追加したファイルstyle.css
が存在しないことがわかります。またmasterブランチ上でgit log
を行うとまだコミットが2つしかないことが確認できます。
~/Desktop/git_tutorial$ git log commit 081f264cc85e5074a93f7304ba2febf9efb90bd9 (HEAD -> master) Author: ushumpei <mail@ushumpei.com> Date: Tue Nov 28 10:17:57 2017 +0800 ページタイトルなのでh1にタグを変更 commit b80d4812cbde6655bee82bfa20127519e8826b81 Author: ushumpei <mail@ushumpei.com> Date: Tue Nov 28 10:02:38 2017 +0800 Initial commit
(補足) なぜブランチを分けるか?全てmaster
にコミットしないのはなぜ?
それは多くの場合、master
ブランチは実際にユーザーが使っているアプリケーションのソースコードになることが多いからです。レビュー、テストを経て開発者みんなで合意したソースコードのみがmaster
に存在します。なのでmaster
ブランチへの変更は完全な状態で行われなければいけません。作業途中のコードを追加するといったことは避けるべきです。しかし開発作業では区切りのいいところでコミットしていく方が、全てのコードを書き終わってからコミットする(ゲームをセーブしないでクリアするようなものです)よりも断然楽です。なので開発作業を行う場所として新たにブランチを作成し、完成したらmaster
にマージするというのがより優れた方法です。
マージ(merge)
現在このリポジトリには2つのブランチが存在しています。master
とstyling
です。流儀にもよりますが、基本的にGitではmaster
ブランチに全ての作業が集約していきます。スタイル設定作業が一旦落ち着いたのでstyling
で行なった仕事をmaster
ブランチに適応しましょう。この別のブランチの変更を別のブランチに適応する作業を マージ(merge) と呼びます。分かれていた枝をくっつけるイメージです。超個人的なイメージでは、master
ブランチからstyling
ブランチの先っぽを掴んで、master
ブランチにくっつけてぎゅっとコブを作る感じです。
styling
をmaster
にマージします。コマンドはgit merge --no-ff styling
になります(--no-ff
はマージした時にコブを作るおまじないです。マージには3つくらい種類がありますが、この方法が複数人の開発では重宝されています)。この時どちらがどちらにマージされるかというのは重要で、自分がmaster
ブランチにいることを確認した上でマージを行なってください。
~/Desktop/git_tutorial$ git status On branch master nothing to commit, working tree clean ~/Desktop/git_tutorial$ git merge --no-ff styling # 実行後適当なエディタが開かれます。使い方が不明なら、vimなら`ZZ`、nanoなら`Ctrl + x`を入力してください Merge made by the 'recursive' strategy. index.html | 1 + style.css | 4 ++++ 2 files changed, 5 insertions(+) create mode 100644 style.css
マージが実行されました。git log --graph
で履歴を見ると、4つのコミットが表示されています。このことはstyling
ブランチにあった「スタイルシートを追加」のコミットがmaster
ブランチに適応されたこと、マージしたという事実を示す新しいマージコミット(merge commit)が作成されたこと、を示しています。
~/Desktop/git_tutorial$ git log --graph * commit c45179e33ebf642db092e04130bbd981078cb6f9 (HEAD -> master) |\ Merge: 081f264 59df46b | | Author: ushumpei <mail@ushumpei.com> | | Date: Tue Nov 28 11:46:57 2017 +0800 | | | | Merge branch 'styling' | | | * commit 59df46ba1df6e9a29584dd2ad2d95b79248f0409 (styling) |/ Author: ushumpei <mail@ushumpei.com> | Date: Tue Nov 28 11:10:48 2017 +0800 | | スタイルシートを追加 | * commit 081f264cc85e5074a93f7304ba2febf9efb90bd9 | Author: ushumpei <mail@ushumpei.com> | Date: Tue Nov 28 10:17:57 2017 +0800 | | ページタイトルなのでh1にタグを変更 | * commit b80d4812cbde6655bee82bfa20127519e8826b81 Author: ushumpei <mail@ushumpei.com> Date: Tue Nov 28 10:02:38 2017 +0800 Initial commit
まとめ
長い文章を読んでくださってありがとうございました。またはまとめを見てくださってありがとうございます。とりあえず基本的な概念の説明を自分なりにまとめて見ただけです。わかりにくい部分、間違っている部分、お気づきになりましたらお知らせ下さい。(コメント、メール、twitter#など最大限対応します)
おまけ
- リポジトリ(repository)は、枝分かれしたブランチ達を持っている(木みたいなもの)
- ブランチ(branch)は、連続したコミット達から成り立っている(枝みたいなもの)
- コミット(commit)は、複数ファイル達に対する変更履歴で出来ている(コブみたいなもの)
といった形です。Gitはこれら3つの対象に対して、様々な操作を行える機能を提供してくれるツールです。Gitは 分散型バージョン管理システム でその目的は 「大勢で1つのアプリケーションを一緒に作ること」 です。今回は バージョン管理システム の側面をクローズアップしてましたので、次回は 分散型 と言われる所以となる機能について書きたいと思います。
最低限必要なGitコマンド
友人に説明する必要性が出てきたので頭の整理を兼ねてメモしておきます。どんなものがあるでしょうか?自分的には最低限以下のやつらかと思います。(merge
とかはプルリクエストで行うので直接打たない想定)
status
, log
, diff
, pull
, push
, checkout
, add
, commit
, reset
めちゃくちゃ主観的なワークフロー的なものを描いて見ました。基本的には、新しいブランチ作る
、コミットいくつか作る
、リモートにブランチをプッシュする
の一連の流れができればいいという話かと思います。
# 現状把握系(随時、癖のように打つ) $ git status # 現在のワーキングディレクトリ、ステージエリアの状態みる $ git log --graph --oneline # ブランチの状態確認 $ git log --graph --oneline --all # リポジトリの状態確認 $ git log -p # 直近何やってたか確認 # ローカル最新化 $ git checkout <branchname> # 最新化の準備(大体master) $ git pull # 最新化 # ブランチ作成とチェックアウト $ git checkout -b <newbranchname> # 作成してチェックアウト $ git branch -d <newbbbbranchname> # タイポしたので削除 # コード書いてコミットを繰り返す $ git diff -w <filename> # 差分確認 $ git add <filename> # ステージ $ git reset HEAD <filename> # ステージやめる $ git checkout <filename> # 変更戻す $ git diff HEAD # コミット前最終確認 $ git commit # コミット # プルリクエスト準備のため、ローカルブランチをリモートにプッシュ $ git push -u origin newbranchname
感想
本当に最低限なので、clone
とか一度やったらあとはやらない系はのぞいています。ファイルの変更取り消し、ステージの取り消し、ときたのでコミット取り消し(reflog
やreset --hard
)もいるかと思ったのですが、余裕ができたらでいい気がします。
参考リンク
あとGitに関する大まかなイメージを持っていると楽かもしれないです。
公開Slackのリンク
今更だけど調べたのでメモ。
公開Slackの作り方は以下のリンクです。Slackin、SlackinをHerokuで(簡単)、Google Apps Script、 Arukas.ioとかが使えそうです。
- slackin: rauchg/slackin: Public Slack organizations made easy
- Heroku + slackin: SlackinでSlackに誰でも参加できるチームをつくる
- Google Apps Script: 公開Slack用 自動招待フォーム by GAS
- Arukas:
Arukas.io で Slack 自動招待フォームβサービス終了で今使えないみたいです(確認: 2017/11/27 14:02)
Google Apps Scriptでやります ちょっとやり方考えます。
追記
Gitで大雑把にConflict解消
こんにちは。
Gitのコンフリクトの解消方法毎回忘れるのでメモしておきます。エディタで修正するのは手間でしたので・・・
$ git checkout --ours filepath # 変更しない $ git checkout --theirs filepath # マージ先のファイルで上書き $ git checkout -m filepath # よくわからなくなったので、コンフリクトが発生した直後の状態に戻す
まとめ
コンフリクトのdiff
が読みにくすぎてやばいです。<<<<<<< HEAD
にHEAD
とか書いてありますがだいたい文字とか見るの面倒なので、diffの囲み(<<<<<<< ... ======= ... >>>>>>>)の中にあるやつは上にあるのが僕らの、下にあるのが奴らのと覚えてます。
<<<<<<< ours(僕らの) ======= theirs(奴らの) >>>>>>>
よくわからなくなったらgit checkout -m .
でやり直しちゃえばいいかなと思います。
あと、個別のファイルに対して変更するかしないかを判断するのはかなりきついな。。。という感想を持ちました。
願わくばコンフリクトが起きない方がいいですが、コンフリクト恐れすぎるのも良くないので、mergetool
とか駆使していきたいです。(Gitはいくらでもやり直しがきくのでやってみてから考えるでいいかと思います)
BitbucketのPipelinesでmasterブランチにPull Request後、自動デプロイ
いい加減デプロイ自動化しないといけない。
プライオリティ低くなってしまっていて放置気味でしたが、毎回サーバーに入ってgit pull
することに飽きてきました。
BitbucketのPipelinesについて調べたことを書いていきます。
無料で使用できますが月50分までの使用制限があるそうなので、そんなにがっつりは使えない気もします。(2017/10/31 時点)
また、この記事ではPipelinesを使いますが、他の選択肢としてはWebhookとかあります。 その場合サーバー側にエントリポイント作って、POST受け取ってあれこれするという風になるかと思います。 状況に応じてWebhookも検討してみると良いかもしれないです。
概要
BitbucketのPipelinesを使えばリポジトリごとに 特定のブランチへのコミットを検知したタイミング で行いたい処理、テスト実行とか通知とかデプロイスクリプト起動とか、を設定できます。
Pipelinesはコミット検知したら裏側でDocker立ち上げているので、私たちはコンテナ内で起動するコマンドを書いていく感じです。立ち上げるDockerイメージも指定できます。
Pipelinesの使い方は簡単で、 bitbucket-pipelines.ymlというYAMLファイルに記述してレポジトリルートにおいておくと勝手に読んでくれます。
プルリクエストのマージじゃなくて 特定のブランチへのコミットを検知したタイミング でいいの?
とか思ったのですがよく考えたら、デフォルトだとプルリクエストをマージするとマージコミットが生成されるため(fast-fowardでもコミット追加されますし)、 masterブランチの設定を記述しておけば、masterへのプルリクエストが閉じられたら本番環境にデプロイ、とか出来る様になります。なるはずです!
やること
- Pipelinesの有効化とbitbucket-pipelines.ymlの記述
- デプロイ用ユーザーを作成
- deploy.shの記述
今回は最小限、デプロイだけです。テストとかロールバックとかについては考えてないので、お気をつけください。
Pipelinesの有効化とbitbucket-pipelines.ymlの記述
Bitbucketへ行き、対象のリポジトリ管理画面の「Settings > Pipelines > Settings」からPipelinesを有効化します。
(リファレンスは次です。参考ページ)
YAMLを書きます。私は次のように記述しました。Dockerイメージは現在使っているサーバーに合わせてcentosにしています。sshできるようにしてデプロイスクリプトを叩くだけです。スクリプトは後で書きます。
image: centos:latest pipelines: branches: master: - step: name: Deploy script: - yum -y install openssh-clients - ssh -p $SERVER_PORT $DEPLOY_USER@$SERVER_IP bash < deploy.sh
$SERVER_PORT, $DEPLOY_USER, $SERVER_IPなどの環境変数を、 「Settings > Pipelines > Environment variables」 から設定します。参考
作成したYAML
ファイルをどうにかmaster
に取り込んでください。好きな方法でいいです。add, commit, pushとか。
デプロイ用ユーザーを作成
デプロイサーバー -> Bitbucket
デプロイサーバーからBitbucketのリポジトリにアクセスするユーザーを作成します。サーバーに接続してbitbucketユーザーを作成します。($DEPLOY_USERと同じもの)
次に新しいユーザーのSSHキーペアを作成してください、これはBitbucketからgit pull
するときに使用します。参考: Creating SSH keys
Bitbucketのサイトから作成した公開鍵を、リポジトリのデプロイキーに設定します。
このユーザーでリモートリポジトリにアクセス(fetch
とか)できたら成功です。
Pipelines -> デプロイサーバー
PipelinesのDockerコンテナからデプロイサーバーにssh接続するために、BitbucketからSSHキーペアを作成します。参考: Use SSH keys in Bitbucket Pipelines
「Settings > Pipelines > SSH keys」 からキーペアを作成してください。
サーバーに戻り、先ほど作ったユーザーの~/.ssh/authorized_keys
に公開鍵を貼り付けます。
またsshが困らないように、この画面の下部にある Known hosts の追加も行っておきましょう。(ポート付きの場合XXX.XXX.XXX.XXX:PORT)
deploy.shの記述
#!/bin/bash REPOSITORY_PATH=/repository/path # 適当に変えてください cd $REPOSITORY_PATH git pull
リポジトリパスに移動してgit pull
するだけのスクリプトです。注意としては、リポジトリのブランチが常にmaster
になっていないと予期せぬ挙動が起こると思います。
完了
適当にブランチ切って修正してコミットしてプッシュしてプルリクエストしてマージして、Pipelinesメニューからタスクが実行されているのが確認できるかと思います。
一旦流れを作っておけば、後からテストを追加とかもできるはずなので、一歩前進というところです。
内容に関して何かございましたらお知らせいただきたいです。こっちの方がいい、ここ間違っている、これめっちゃ危ない、根本的に勘違いしている、ちゃんとリファレンス読みなさい、など指摘いただけると大変助かります。
ハマった
- リポジトリの権限
- Permission denied
ssh-agent
が止まっているとか~/.ssh
周りの権限とか所有者が適切ではなかったり- どの時点での権限不足なのか、サーバー3つ(Pipelines, Deploy, Bitbucket)なのでわかりにくかった
- Pipelinesを使い始める順番
React NativeでARを体感する、Introducing Expo AR
9月末にExpo
がアップデートされてiOS
のAR Kit
対応が行われました。チュートリアルが公開されたのでちょっと手を出してみました。使用する要素としては、
- expo
- three
- expo-three
のみです。3Dオブジェクトの生成はThree.js
、レンダリングはExpo
、Expo Three
という構成になっています。
Introducing Expo AR: Three.js on ARKit
デモ
感想
Expoの制限はあるもののARアプリをJavaScript
でかけるというのは前提となる知識量のハードルが下がるかと思います。
チュートリアルやると満足感が得られてしまうのでやや危険ですね。色々いじってみるのは大切な一歩だと思いますが、一歩踏み出した後に自分の発想が広がっていかないのがなかなか辛いところです。
サービスがSSL化するとき
ものすごく短い話ですが、サービスがSSL化するときの対応を想定してみました。近々知り合いのサイトにSSL入れられることになりそうなのが理由です。そういえばはてなブログもSSL化するそうですね!
概要
http
をhttps
に書き換えるの面倒ですよね。省略できた気がします。(確かにできるみたいです参考: リンクのhttp:
やhttps:
を省略して現在のプロトコルでリンク先にアクセスさせる)。ただ参考リンクのタイトル通り、通信は現在のスキームになるのでほおっておくと 本来SSLが必要なページでもhttp
で接続されてしまいます。というのはかなり問題だと思うので、移行するときは以下の切り分けをするかもしれないです。
切り分け方法: 追記(2017年10月3日)
- 内部
- リソースとかユーザーの情報を扱わないリンクに関しては、スキーム省略
- 明示的にssl化したページはしっかり
https
書いておく
- 外部
- リンク先に合わせたスキームを書く
感想
でも切り分けるくらいならhttps
に置換しちゃったほうが早くない?というと、それもそうだと思います。前に、「画面動かない!直して!->ssl
切れちゃってました」、というhttps
-> http
現象があったので対応減らせたらいいなと思った次第です。
混在コンテンツ(Mixed Content)に対しても効果があるそうです。