ushumpei’s blog

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

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

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

GraphQL!(こんにちは)

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

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

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

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

目次

Apolloプロジェクトの概要

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

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

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

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

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

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


ReactApollo

完成品のソースです。

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

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

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

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

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

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

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

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

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

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

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

type Query {
  users: [User]
}
`;

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

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

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

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

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

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

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

参考

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

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

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

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

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

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

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

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

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

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

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

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

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

export default graphql(usersListQuery)(UsersList);

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

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

...

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

...

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

export default App;

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

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

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

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

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

... // typeDefs

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

export default schema;

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

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

const PORT = 4000;

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

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

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

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

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

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


graphiql

query {
  users {
    id
    name
  }
}

参考

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

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

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

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

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

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

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

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

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

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

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

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

type Query {
  users: [User]
}

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

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

query {
  users {
    id
    name
  }
}

=>

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

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

server/src/schema.jsの変更

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

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

server/src/resolver.jsの変更

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

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

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

=>

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

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

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

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

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

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

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

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

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

export default graphql(addUserMutation)(AddUser);

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

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

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

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

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

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

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

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

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

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

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

まとめ

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

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

感想

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

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

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

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

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

MockNetworkの作成

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

customResolversがcacheResolversに変わっていた

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

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

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

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

WebSocketの設定

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

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

という感じです。

参考: addGraphQLSubscriptions is removed from 0.9.0 version?

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

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

分類

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

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

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

前提

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

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

プラグインとして配置

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

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

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

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

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

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

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

パッケージとして配置

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

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

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

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

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

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

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

まとめ

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

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

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

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

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

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

目次

動機

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

リファレンス

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

作るもののイメージ

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

"入力
:Mdtable 2 3

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

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

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

書いて見た

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

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

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

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

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

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

f:id:ushumpei:20171203120943g:plain

学んだことの詳細

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

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

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

キーマッピング

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

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

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

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

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

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

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

引数チェック

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

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

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

感想

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

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

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

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

Vim Advent Calendar 2017

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

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

目次

Apollo Client

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

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

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

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


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

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

Cache関連

apollo-cache-inmemory

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

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

Link関連

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

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

import { getOperationAST } from 'graphql';

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

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

const wsLink = new WebSocketLink(new SubscriptionClient('ws://localhost:4000/subscriptions', { reconnect: true }));

const httpLink = new HttpLink({ uri: 'http://localhost:4000/graphql' });

const link = ApolloLink.split(
  ({ query, operationName }) => {
    const operationAST = getOperationAST(query, operationName);
    return !!operationAST && operationAST.operation === 'subscription';
  },
  wsLink,
  httpLink,
);

subscriptions-transport-ws

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

GraphQL関連

graphql-tag

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

import gql from 'graphql-tag';

...

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

GraphQL.js

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

graphql-tools

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

import { makeExecutableSchema } from 'graphql-tools';

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

React関連

react-apollo

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

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

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

まとめ

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

読み物

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

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

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

とても好きです

Fine Software Writings

Fine Software Writings

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

Joel on Software

Joel on Software -

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

リーダブルコード

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

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

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

情熱プログラマー

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

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

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

SOFT SKILLS

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

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

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

ハッカーと画家

Hackers and Painters

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

リーン・スタートアップ

リーン・スタートアップ

リーン・スタートアップ

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

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

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

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

徳丸本

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

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

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

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

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

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

まとめ

随時更新していきます。