20190411のNode.jsに関する記事は5件です。

node.jsを触るために簡単なチャットシステムを作る(サーバー接続編)

Node.js を触ってみたいと思ったので、備忘録も兼ねて以下に記します。
よりよい方法やバグ等ございましたら、アドバイスいただけると光栄です。

今回は「サーバー接続編」ということで、クライアントとサーバーの接続処理をやっていきます。

※前回 node.jsを触るために簡単なチャットシステムを作る(環境構築編) という表題で、環境構築をしていますので、環境構築がまだな方はこちらを参照ください。

サーバーを起動する

まずは、mychat フォルダに、server.js というファイルを作成します。

$ cd mychat
$ mkdir server.js

ファイルが作成出来たら、server.js を下記のようにします。

'use strict';

// モジュール
const http     = require('http');
const express  = require('express');
const socketIO = require('socket.io');
const moment   = require('moment');

// オブジェクト
const app    = express();
const server = http.Server(app);
const io     = socketIO(server);

// 定数
const PORT = process.env.PORT || 3000;

// サーバーの起動
server.listen(
    PORT,
    () => {
        console.log('server starts on port: %d', PORT);
    });

動作を確認する

下記コマンドで実行すると動作の確認が出来ます。

$ node server
server starts on port: 3000

3000番ポートでサーバーが立ち上がったことが分かるかと思います。
尚、起動したサーバーは、「Ctrl + C」で終了します。

HTMLファイルを表示する

実際にビュー側に表示させるHTMLファイル(/public/index.html)を作成します。

$ mkdir -p public/index.html

ファイルが作成出来たら、/public/index.html を下記のようにします。

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>mychat</title>
</head>
<body>
    <h1>node.js を触ってみた</h1>
</body>
</html>

表示するHTMLファイルを上記の public/index.html に指定する為
server.js の「サーバーの起動」処理の前に、以下の処理を追加します。

// 公開フォルダの指定
app.use(express.static(__dirname + '/public'));

動作を確認する

サーバーを立ち上げた状態で、
http://localhost:3000 にアクセスすると
test.png
このように表示されれば完了です。

クライアントとサーバーを接続する

サーバーへの接続要求処理、接続時の処理を記載するJSファイル(/public/client.js)を作成します。

$ mkdir public/client.js

ファイルが作成出来たら、/public/client.js を下記のようにします。

// クライアントからサーバーへの接続要求
const socket = io.connect();

// 接続時の処理
socket.on(
    'connect',
    () => {
        console.log('connect');
    });

上記の client.js ファイルを反映させる為
public/index.html<body> の末尾に、以下の処理を追加します。

<script src="/socket.io/socket.io.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="client.js"></script>

server.js の「公開フォルダの指定」処理の前に、以下の「接続時の処理」を追加します。

// 接続時の処理
io.on(
    'connection',
    (socket) => {
        console.log('connection');
    });

server.js 全体としては、以下のようになります。

'use strict';

// モジュール
const http     = require('http');
const express  = require('express');
const socketIO = require('socket.io');
const moment   = require('moment');

// オブジェクト
const app    = express();
const server = http.Server(app);
const io     = socketIO(server);

// 定数
const PORT = process.env.PORT || 3000;

// グローバル変数
let iCountUser = 0; // ユーザー数

// 接続時の処理
io.on(
    'connection',
    (socket) => {
        console.log('connection');
    });

// 公開フォルダの指定
app.use(express.static(__dirname + '/public'));

// サーバーの起動
server.listen(
    PORT,
    () => {
        console.log('server starts on port: %d', PORT);
    });

動作を確認する

サーバーを立ち上げた状態で、
http://localhost:3000 にアクセスします。
test2.png

デベロッパーツールの Console に、connect と表示(画像赤線)されます。

また、サーバー側では connection と表示されれば完了です。

$ node server
server starts on port: 3000
connection

以上で、クライアントとサーバーの接続に関する基本処理が完了です。
次回は実際にメッセージを送信してみます。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Node.js で Oracle DBのテーブル変更を検知する

TL;DR

データベース変更通知機能 Oracle Database Continuous Query Notification を利用して実現可能。
https://docs.oracle.com/database/121/JJDBC/dbchgnf.htm#JJDBC28815

データが変わったことをプログラム側で検知する

データが変わったことを検知する方法は、大きく分けてPullPushの2通りが考えられます。

  1. Pullは、クライアントプログラムで一定時間ごとにデータを取得し、データが変わっているかどうかを判定します。主にポーリングと呼ばれます。
  2. Pushは、データが変わったことをクライアントプログラムにサーバーから通知してもらう方法です。通知受付用APIやリスナーをクライアント側に用意します。

それぞれの良し悪しはありますが、今回はデータ変更の検知スピードが重要視されていたのでPushで実現方法を検討します。

DBサーバーで変更を検知する(TRIGGER) -> 失敗

データの変更といえばTRIGGERですね。
変更を検知したらクライアントプログラムにHTTPで通知します。

CREATE OR REPLACE TRIGGER TRIGGER_HOGE_TABLE
AFTER INSERT OR UPDATE OR DELETE
ON HOGE_TABLE
DECLARE
  vResponse VARCHAR2(100);
  v_errcode number;
  v_errmsg  varchar2(100);
BEGIN
  INSERT INTO HOGE (HOGE) VALUES ('start');
  vResponse := UTL_HTTP.REQUEST('http://162.168.0.2:3000/fuga');
  INSERT INTO HOGE (HOGE) VALUES (vResponse);
EXCEPTION
  WHEN OTHERS THEN
    v_errcode := sqlcode;
    v_errmsg  := substr(sqlerrm, 1, 100);
    INSERT INTO HOGE (HOGE) VALUES ('error');
    INSERT INTO HOGE (HOGE) VALUES (TO_CHAR(v_errcode));
    INSERT INTO HOGE (HOGE) VALUES (v_errmsg);
END;
/

Oracle 11g以降でUTL_HTTP.REQUESTを使うためには権限が必要です。

ORA-29273: HTTP request failed
ORA-24247: network access denied by access control list (ACL)
BEGIN
    DBMS_NETWORK_ACL_ADMIN.CREATE_ACL (
    ACL => 'NETWORK_ACL_HOGE',
    DESCRIPTION => 'ACL for REST',
    PRINCIPAL => 'スキーマ名',
    IS_GRANT => TRUE,
    PRIVILEGE => 'connect',
    START_DATE => NULL,
    END_DATE => NULL);
    COMMIT;
END;
/
BEGIN
DBMS_NETWORK_ACL_ADMIN.assign_acl (
acl => '/sys/acls/acl_smtp.xml',
host => 'ホスト名',
lower_port => 3001,
upper_port => NULL);
END;
/
BEGIN
DBMS_NETWORK_ACL_ADMIN.ADD_PRIVILEGE(
acl => 'NETWORK_ACL_1F858DA0B2597454E0538FF412AC543C',
principal => 'スキーマ名',
is_grant => TRUE,
privilege => 'connect');
END;
/
SELECT * FROM DBA_NETWORK_ACLS;

SELECT * FROM DBA_NETWORK_ACL_PRIVILEGES;

なんやかんや頑張った挙句、トリガーが発動するタイミングはコミット時ではないということに気付く。
TRIGGERはデータが変わったタイミングで発動するので、その時点ではまだコミットされていない。
クライアントに通知された後でロールバックされると、ピンポンダッシュになる。

コミット時にリフレッシュされるMaterialized Viewを使って、無理やり実現することはできたが、どうもにも気持ちが悪い。しかも遅い。

DBサーバーで変更を検知する(CQN) -> 本題

データベース変更通知機能 Oracle Database Continuous Query Notification を利用して実現できそうだったので、これを試してみることにした。
https://docs.oracle.com/database/121/JJDBC/dbchgnf.htm#JJDBC28815

まずは権限付与。

GRANT CHANGE NOTIFICATION TO HOGE_SCHEMA;

DB側の設定はこれで終わり。
クライアント側から監視対象を登録する形となる。
今回のクライアントプログラムはNode.jsだったので、node-oracledbを利用することになる。

https://github.com/oracle/node-oracledb

こちらの記事を参考に…
https://blogs.oracle.com/opal/demo-oracle-database-continuous-query-notification-in-nodejs

// Oracle DB
const oracledb = require("oracledb");

// Return Object
oracledb.outFormat = oracledb.OBJECT;
oracledb.fetchAsString = [ oracledb.DATE, oracledb.NUMBER ];

// Continuous Query Notification(CQN) Event Mode
oracledb.events = true;

// Connection Pooling
const pool = await oracledb.createPool({
"user": "HOGE_SCHEMA",
"password": "PIYO",
"connectString": `
  (DESCRIPTION =
    (ADDRESS_LIST =
      (ADDRESS = (PROTOCOL = TCP)(HOST = 168.0.0.3)(PORT = 1234))
    )
    (CONNECT_DATA =
      (SERVICE_NAME = hogedb)
    )
  )
`,
"poolMax": 20,        // 最大プール数
"poolMin": 2,         // 最小プール数
"poolIncrement": 1,   // 足りない場合に増やす数
"poolTimeout": 60,    // プールが未使用の場合にクローズするまでの秒数[default:60] 最小プール数は維持される
"queueTimeout": 60000 // 接続要求キューで待機している処理のタイムアウト ミリ秒[default:60000 ms]
});

// CQN
const cqn_con = await pool.getConnection();
await cqn_con.subscribe("hogesub", {
"callback": async message => {
  // 登録解除イベントは処理しない
  if (!message || !message.type || message.type == oracledb.SUBSCR_EVENT_TYPE_DEREG) {
    return;
  }

  // HOGEのイベント通知
  if (message.tables.some(val => val.name == "HOGE_SCHEMA.HOGE")) {
    // HOGEテーブルを取得して処理する
  }

  // FUGAのイベント通知
  if (message.tables.some(val => val.name == "HOGE_SCHEMA.FUGA")) {
    // FUGAテーブルを取得して処理する
  }

  // PIYOのイベント通知
  if (message.tables.some(val => val.name == "HOGE_SCHEMA.PIYO")) {
    // PIYOテーブルを取得して処理する
  }

  // HOGERAのイベント通知
  if (message.tables.some(val => val.name == "HOGE_SCHEMA.HOGERA")) {
    // HOGERAテーブルを取得して処理する
  }
},
"sql": "SELECT * FROM HOGE WHERE STATUS = 'Use'",
"port": 3002,
"groupingClass" : oracledb.SUBSCR_GROUPING_CLASS_TIME,
"groupingValue" : 1, // 1秒以内の通知はまとめる
"groupingType" : oracledb.SUBSCR_GROUPING_TYPE_SUMMARY
});

// 監視対象2個目以降
await Promise.all([
cqn_con.subscribe("hogesub", { "sql": "SELECT * FROM FUGA" }),
cqn_con.subscribe("hogesub", { "sql": "SELECT * FROM PIYO WHERE STATUS = 'Use'" }),
cqn_con.subscribe("hogesub", { "sql": "SELECT ID,SCRIPT_NAME,DESCRIPTION FROM HOGERA WHERE DELETE_FLG = 0" })
]);

:
:
// プログラム終了時(監視終了)
await cqn_con.unsubscribe("hogesub");
await cqn_con.release();

これで対象のテーブルに対して変更(コミット)があった場合に通知と処理をすることが出来るようになりました。
しかもかなり早く検知してくれる。

監視対象の登録は以下で確認できる。

SELECT * FROM USER_CHANGE_NOTIFICATION_REGS

余談

タイムアウトまたは、初回受けたらパージするオプションを入れていない場合、DB側に登録が残り続ける。
プログラム側で解除をするのが正しいが、もし忘れた場合は上のSQLで確認したREGIDを指定して解除する。

BEGIN
  DBMS_CQ_NOTIFICATION.DEREGISTER(21);
END;

解除する…。いやいや、解除できないんだけど。

ORA-29970: Specified registration id does not exist
ORA-06512: at "SYS.DBMS_CHANGE_NOTIFICATION", line 3
ORA-06512: at "SYS.DBMS_CHANGE_NOTIFICATION", line 72
ORA-06512: at line 4

Oracleパッチが当たっていないと解除できないらしい。
https://stackoverflow.com/questions/46831869/delete-oracle-change-notifications

REVOKE CHANGE NOTIFICATION FROM HOGE_SCHEMA;

権限をはく奪すれば、無事全部消えました。セーフ。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

discord.js + canvasjsでユーザーの入室通知を作る

はじめまして、Discord.jsでdiscordのBOTを作っている者です。
まだまだ初心者ですが、よろしくお願いします。

※説明が下手くそです、すみません

手始めに

まずNode.jsをインストールしてください。
(自分の場合はUbuntuなのでUbuntuでやる方法を教えます。)
1. ターミナルを開いてください
2. ターミナルに〈sudo apt install -y nodejs npm〉を打ち込みます
3. パッケージのダウンロード、インストールが開始されるのでコーヒーでも飲んで待ってましょう
4. インストールが終わったら、これでnode.js導入は完了です。

必要なパッケージのインストール

(コマンドはnpm install <パッケージ名>です)
・discord.js
・canvas
・snekfetch

コードを書く

パッケージのインストールが終わったら、コードを書き始めます。

index.js
const Discord = require('discord.js');
const client = new Discord.Client
const Canvas = require('canvas');
const snekfetch = require('snekfetch');

client.on('ready', () => {
  console.log("起動しました。")
 });

client.on('guildMemberAdd', async member => {
    const channel = member.guild.channels.find(ch => ch.name === 'member-log');//member-logというチャンネルを作ると、そこにメッセージが送信される
    if (!channel) return;

    const canvas = Canvas.createCanvas(1050, 250);
    const ctx = canvas.getContext('2d');

    const background = await Canvas.loadImage('./背景画像.jpg');

    ctx.drawImage(background, 0, 0, canvas.width, canvas.height);


ctx.strokeStyle = '#74037b';
    ctx.strokeRect(0, 0, canvas.width, canvas.heigh)

  ctx.font = '28px DejaVu';
    ctx.fillStyle = '#ffff00';
    ctx.fillText(`Welcome to ${member.guild.name},`, canvas.width / 3.0, canvas.height / 3.5);

  ctx.font = '28px DejaVu';
    ctx.fillStyle = '#ffff00';
    ctx.fillText(`${member.user.tag}!`, canvas.width / 3.0, canvas.height / 1.8);

  ctx.font = '28px DejaVu';
    ctx.fillStyle = '#ffff00';
    ctx.fillText(`id: ${member.user.id}`, canvas.width / 3.0, canvas.height / 1.3);

    ctx.beginPath();
    ctx.arc(125, 125, 100, 0, Math.PI * 2, true);
    ctx.closePath();
    ctx.clip();

    const { body: buffer } = await snekfetch.get(member.user.displayAvatarURL);
    const avatar = await Canvas.loadImage(buffer);
    ctx.drawImage(avatar, 25, 25, 200, 200);

  const attachment = new Discord.Attachment(canvas.toBuffer(), 'welcome-image.png');

    channel.send(`ようこそ ${member}さん!`, attachment);
});

client.login("トークン");

トークンを取得

client.login("トークン");

この〈トークン〉はここから取得します(https://discordapp.com/developers/applications/)
1. URLにアクセス
2. Discordにログイン
3. New Applicationを押し、BOTの名前を決める、そしてcreateを押す
4. 左側のBOTを押し、Add Botを押す、警告みたいなのが出るので、Yes, do it!を押す
5. 色々表示されるので、その中のCopyを押して、トークンのところにペーストします

これでnode index.jsと打てば起動するはずです。
初めての投稿だったのでガバガバなところが多いと思いますが、暖かい目で見守ってください。
それでは。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

GraphQLでスキーマ駆動開発導入したら開発効率がアップするぞ!!

現在携わっているプロジェクトにGraphQL を使ってスキーマ駆動開発を導入したんですけど、かなり開発効率もよくなおかつ品質向上に良いのでやったことのまとめとして記事にしてみます。

これからスキーマ駆動開発を導入を検討している方の後押しになれば幸いです。

スキーマ駆動以前の開発だと何が辛いのか

どうしてもフロントエンド開発の着手が遅くなりがちという点があると思います。
基本的には設計 > API実装 > フロント実装という流れなんですよね。さらに開発を進めていく中でうまく行かない部分があればAPIの実装・修正が必要になる場合もあります。そうするとさらにフロントの実装は遅れてしまいます。

あとはAPIの実装がこんな感じになりそうだから、そうなるていでフロントも実装したりも。んで、あとで出戻りがあったりして開発効率としては良くないですね。

スキーマ駆動開発だと何が嬉しいのか

  • API開発とフロント開発を同時に進めることができる
  • スキーマがドキュメントとして存在するので出戻りがない(少ない)
  • スキーマを元に型定義するので品質もアップ

あとは直接スキーマ駆動開発の利点ではないのですが、後述の graphql-codegen を使うとコマンド一つでスキーマから型定義ファイルを生成することができ、さらに開発効率がアップします。

開発手順のおさらい

まずスキーマ駆動の開発手順としてはざっくり以下のよう流れになっています。

  1. スキーマの定義
  2. mock作成
  3. フロント、サーバ共に実装
  4. テスト or リリース

以下、開発手順と照らし合わせながら記事を進めます。

プロジェクトの構成図

基本方針として以下のことが決まっていました。

  • GraphQLを使う
  • サーバサイド(BFF)はnodeで
  • フロントはReact
  • すでにマイクロサービスはある

image.png

スキーマ

まずはスキーマの定義を行います。
例えばこんな感じで書きます。今回はわかりやすくUser一覧とUser単体の取得系とログインするスキーマを定義してみます。

schema.graphql
scalar Date
scalar DateTime
scalar Error
scalar EmailAddress
scalar URL

type Query {
  user(id: ID!): User
  users: [User]
}

type Mutation {
  login(email: String!, password: String!): AuthResponse!
}

type AuthResponse {
  success: Boolean
  error: Error
  token: String
}

type User {
  id: ID!
  name: String
  email: EmailAddress
  registerDate: DateTime
  profileImageUrl: URL
}

上記スキーマでOKならば次にqueryとmutationの作成をします。

user.graphql
query user($id: ID!) {
  user(id: $id) {
    id
    name
    profileImageUrl
    registerDate
  }
}

query users {
  users {
    id
    name
    registerDate
    profileImageUrl
  }
}

login.graphql
mutation loginStaff($email: String!, $password: String!) {
  login(email: $email, password: $password) {
    token
  }
}

で次にTypeScriptであれば型の定義したり、resolverの型定義だったりしなければいけません。これが意外と面倒なんですよね。 graphql-codegen を使うことでこの辺りの面倒な作業を完全に省けます。

というのもスキーマさえあればコマンド一つでTSの型定義やらuseQuery(後述)、useMutation(後述)などを生成をしてくれます。これについてもスキーマ駆動開発との相性がよかったなと思っています。スキーマを定義したらフロントのViewでqueryとmutationが使える状態です。

詳しいやり方は公式をみてもらうとして

codegenの設定ファイルは以下のようにしました。出力先として server/client/ を指定しています。

codegen.yml
overwrite: true
schema: './graphql/schema.graphql' // スキーマの場所
documents: './graphql/**/*.graphql' //  queryとかmutationの定義の場所
generates:
  ../server/src/gen/types.ts: // 出力先 こっちはserver側
    plugins:
      - 'typescript'
      - 'typescript-resolvers'
  ../client/src/gen/actions.tsx: // 出力先 こっちはclient側
    plugins:
      - 'typescript'
      - 'typescript-operations'
      - 'typescript-react-apollo'
    config:
      withComponent: false
      withHooks: true
      withHOC: false

server/src/gen/types.ts

かなり省略してるけど概ねこんな感じで生成されます。

types.ts
export type AuthResponse = {
  success?: Maybe<Scalars['Boolean']>
  error?: Maybe<Scalars['Error']>
  token?: Maybe<Scalars['String']>
}

export type Mutation = {
  login: AuthResponse
}

export type MutationLoginArgs = {
  email: Scalars['String']
  password: Scalars['String']
}

export type Query = {
  user?: Maybe<User>
  users?: Maybe<Array<Maybe<User>>>
}

export type QueryUserArgs = {
  id: Scalars['ID']
}

export type User = {
  id: Scalars['ID']
  name?: Maybe<Scalars['String']>
  email?: Maybe<Scalars['EmailAddress']>
  registerDate?: Maybe<Scalars['DateTime']>
  profileImageUrl?: Maybe<Scalars['URL']>
}


export type MutationResolvers<Context = any, ParentType = Mutation> = {
  login?: Resolver<AuthResponse, ParentType, Context, MutationLoginArgs>
}

export type QueryResolvers<Context = any, ParentType = Query> = {
  user?: Resolver<Maybe<User>, ParentType, Context, QueryUserArgs>
  users?: Resolver<Maybe<Array<Maybe<User>>>, ParentType, Context>
}

export type UserResolvers<Context = any, ParentType = User> = {
  id?: Resolver<Scalars['ID'], ParentType, Context>
  name?: Resolver<Maybe<Scalars['String']>, ParentType, Context>
  email?: Resolver<Maybe<Scalars['EmailAddress']>, ParentType, Context>
  registerDate?: Resolver<Maybe<Scalars['DateTime']>, ParentType, Context>
  profileImageUrl?: Resolver<Maybe<Scalars['URL']>, ParentType, Context>
}

export type Resolvers<Context = any> = {
  AuthResponse?: AuthResponseResolvers<Context>
  Mutation?: MutationResolvers<Context>
  Query?: QueryResolvers<Context>
  User?: UserResolvers<Context>
}

export type IResolvers<Context = any> = Resolvers<Context>

client/src/gen/actions.ts

今回は、hooksのみで実装しようと思っていたので react-apollo-hooks 使います。codegen.yml の withHooks をtrueにすることで生成されます。Apolloのcomponentタイプなども選べるのでフロントの実装に合わせて生成が可能です。

actions.ts
type Maybe<T> = T | null
/** All built-in and custom scalars, mapped to their actual values */
export type Scalars = {
  ID: string
  String: string
  Boolean: boolean
  Int: number
  Float: number
  EmailAddress: any
  DateTime: any
  URL: any
  Error: any
  Date: any
}

export type AuthResponse = {
  success?: Maybe<Scalars['Boolean']>
  error?: Maybe<Scalars['Error']>
  token?: Maybe<Scalars['String']>
}

export type Mutation = {
  login: AuthResponse
}

export type MutationLoginArgs = {
  email: Scalars['String']
  password: Scalars['String']
}

export type Query = {
  user?: Maybe<User>
  users?: Maybe<Array<Maybe<User>>>
}

export type QueryUserArgs = {
  id: Scalars['ID']
}

export type User = {
  id: Scalars['ID']
  name?: Maybe<Scalars['String']>
  email?: Maybe<Scalars['EmailAddress']>
  registerDate?: Maybe<Scalars['DateTime']>
  profileImageUrl?: Maybe<Scalars['URL']>
}
export type LoginMutationVariables = {
  email: Scalars['String']
  password: Scalars['String']
}

export type LoginMutation = { __typename?: 'Mutation' } & {
  login: { __typename?: 'AuthResponse' } & Pick<AuthResponse, 'token'>
}

export type UserQueryVariables = {
  id: Scalars['ID']
}

export type UserQuery = { __typename?: 'Query' } & {
  user: Maybe<{ __typename?: 'User' } & Pick<User, 'id' | 'name' | 'profileImageUrl' | 'registerDate'>>
}

export type UsersQueryVariables = {}

export type UsersQuery = { __typename?: 'Query' } & {
  users: Maybe<Array<Maybe<{ __typename?: 'User' } & Pick<User, 'id' | 'name' | 'registerDate' | 'profileImageUrl'>>>>
}

import gql from 'graphql-tag'
import * as ReactApolloHooks from 'react-apollo-hooks'

export const LoginDocument = gql`
  mutation login($email: String!, $password: String!) {
    login(email: $email, password: $password) {
      token
    }
  }
`

export function useLoginMutation(
  baseOptions?: ReactApolloHooks.MutationHookOptions<LoginMutation, LoginMutationVariables>
) {
  return ReactApolloHooks.useMutation<LoginMutation, LoginMutationVariables>(LoginDocument, baseOptions)
}
export const UserDocument = gql`
  query user($id: ID!) {
    user(id: $id) {
      id
      name
      profileImageUrl
      registerDate
    }
  }
`

export function useUserQuery(baseOptions?: ReactApolloHooks.QueryHookOptions<UserQueryVariables>) {
  return ReactApolloHooks.useQuery<UserQuery, UserQueryVariables>(UserDocument, baseOptions)
}
export const UsersDocument = gql`
  query users {
    users {
      id
      name
      registerDate
      profileImageUrl
    }
  }
`

export function useUsersQuery(baseOptions?: ReactApolloHooks.QueryHookOptions<UsersQueryVariables>) {
  return ReactApolloHooks.useQuery<UsersQuery, UsersQueryVariables>(UsersDocument, baseOptions)
}

mockサーバー

Apollo Serverで簡単に作れます。てかこれだけです。
さらに、レスポンスをカスタマイズも簡単にできます。詳しいやり方はMocking - Apollo Docsを見てみてくださいね。

const { ApolloServer, gql } = require('apollo-server');

const typeDefs = gql`
  type Query {
    hello: String
  }
`;

const server = new ApolloServer({
  typeDefs,
  mocks: true,
});

server.listen().then(({ url }) => {
  console.log(`? Server ready at ${url}`)
});

マイクロサービス

User, Admin ・・・といった具合にマイクロサービス化しています。

BFF (バックエンド For フロントエンド)

簡単に技術スタックをご紹介すると・・・
Node + koa + Apollo + TypeScript って感じです。GraphQLをBFFで使うこともできたのもマイクロサービスがすでにあったのが大きいですね。今回のプロジェクトにGraphQLはかなりマッチしていました。

Apollo-Serverを使ってGraphQLサーバを立ち上げるのですが、http://localhost:4000/graphql など任意のURLでGraphiQL IDEが立ち上がるのでブラウザ上で確認をしながら実装することができます。

const Query: QueryResolvers = {
  talent(obj, args, context, info) {
    // マイクロサービスからかき集めてデータを返す
  },
  users(obj, args, context, info) {
    // マイクロサービスからかき集めてデータを返す
  }
}

const Mutation: MutationResolvers = {
  async login(_obj, arg: { email: string; password: string }) {
    const { password, email } = arg
    // 何かしらの処理
  }
}

const resolvers: Resolvers = {
  Query,
  Mutation,
}

export default resolvers

フロントエンド

React + Apollo + TypeScript で実装をしています。

実際の開発の流れとしてはまずは前述のmockサーバで実装、resolverができたらdevサーバで開発・確認をして進めます。

さらに、今回はwithHooksを使って取得・更新をするようにしました。graphql-codegenuseUsersQueryuseLoginMutation を生成してくれるのでフロントでは使うだけです。このwithHooksが結構よかったですね。Componentと機能ごとに作ることができその中に閉じ込めることができます。個人的にはAtomic Designと合わせるさらに良いかなと。

const Users: React.FunctionComponent<{}> = () => {
  const { data, loading } = useUsersQuery()
  if (loading) return <div>Loading</div>
  if (Object.keys(data).length < 1) return null
  return (
    <React.Fragment>
      {data.talents.map((e, key) => {
        return (
          <div>
            // 何かしら表示
          </div>
        )
      })}
    </React.Fragment>
  )
}
const LoginContent: React.FunctionComponent<{}> = () => {
  const onSubmitSignIn = useLoginMutation({
    update: (_, { data }) => {
      // mutationでsuccess後の処理
    }
  })

  return (
    // form
  )
}

余談だけどReduxはいらない

GraphQLを使うことでReduxはいよいよ必要ないかなと。そもそもサーバ側とReduxで同じデータを管理していたようなものですし。さらに、GraphQLというかこの場合はApolloなのかな、エラーハンドリングも UNAUTHENTICATEDINTERNAL_SERVER_ERROR のコードを返してくれるので、Toastなんかと組み合わせてそこまでコストかからずに実装できます。

テストについて

基本的にスキーマから生成しているものなので、型やデータの整合性は取れています。さらにGraphQLエラーとしてValidationもしてくれるので、従来の開発スタイルよりもかなり品質は向上しますね。

ただ、ログインのMuationのようにemailやpasswordをvariablesとして渡す必要がある場合にはスキーマの変更がフロントまで行き届かないです。

例えばスキーマで email > emailAddress というように変更があったとして、型定義やらwithHooksなどは生成し直してくれるわけなんですが、View側で

useLoginMutation({variables: {email: '', password: ''}})

としている場所は勝手に変更されることはないです。ま、当然といえば当然何ですが。あまり頻発するようなことはないでしょうけどこのあたりの変更をテストで間違ってリリースしてしまうのを防ぐ必要はありそうです。

スナップショットなどをとってViewの差分を見ても良いんですけど、これはリリースした後にゆっくり導入していければ。

色々試したんですけど、 easygraphql-tester 一番簡単でわかりやすかったので。あとはCircle Ciなど使ってテストしていければいいですよね。

describe('A user', function() {
  let tester
  beforeAll(() => {
    tester = new EasyGraphQLTester(schemaCode)
  })

  test('UsersDocument', () => {
    tester.test(true, UsersDocument)
  })
  test('UserDocument', () => {
    tester.test(true, UserDocument, { id: 1 })
  })
  test('LoginDocument', () => {
    tester.test(true, LoginStaffDocument, { email: 'example@gmail.com', password: 'hgoehoge' })
  })
})

とはいえ問題点もある

プロジェクトリリース後にスケールした時や変更があった時ににスキーマ管理(バージョン含め)が今後のリリース後の課題感としてチームで話が上がっています。その辺りも今後記事にできればと思っています。

とはいえGraphQLを初めてプロジェクトに導入した結果、スキーマ駆動開発のおかげで作業効率もよく、品質が向上したのは良いことだと思っています。

BFFとの相性が良いのは実感できたが、そうじゃない場合はどうなんだろってのはあるので機会があれば試したいみたいです。

今後GraphQLの事例がもっと増えてくれると良いなと。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Python使わないけどPythonが欲しい(Ubuntu)

Ubuntu環境(Ubuntu Server 18.04.2 LTS) をインストールして、node環境を構築し、いざnpmでパッケージをインストールしようとすると、
python_err.png
「python」が見つからないエラーが出る事あると思います。
んで、ググってみるとよく見かけるのが、

 ・Ubuntuには最初からPythonが入っている
  →「python3」コマンドで起動できます。でもnpmインストールに必要なのは2.7系。

 ・Ubuntu2.7系をインストールするには「sudo apt-get install python2.7」
  →「python2」で起動できます。でもnpmインストール時エラーは回避出来ません。

「ただnpmインストール時のエラーを回避したいだけ」の方は、
このコマンドでインストールすれば、npmからpythonが使える様になります。

sudo apt install python-minimal
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む