- 投稿日:2019-04-11T23:31:22+09:00
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: 30003000番ポートでサーバーが立ち上がったことが分かるかと思います。
尚、起動したサーバーは、「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 にアクセスすると
このように表示されれば完了です。クライアントとサーバーを接続する
サーバーへの接続要求処理、接続時の処理を記載する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 にアクセスします。
デベロッパーツールの Console に、
connectと表示(画像赤線)されます。また、サーバー側では
connectionと表示されれば完了です。$ node server server starts on port: 3000 connection以上で、クライアントとサーバーの接続に関する基本処理が完了です。
次回は実際にメッセージを送信してみます。
- 投稿日:2019-04-11T22:17:55+09:00
Node.js で Oracle DBのテーブル変更を検知する
TL;DR
データベース変更通知機能 Oracle Database Continuous Query Notification を利用して実現可能。
https://docs.oracle.com/database/121/JJDBC/dbchgnf.htm#JJDBC28815データが変わったことをプログラム側で検知する
データが変わったことを検知する方法は、大きく分けて
PullとPushの2通りが考えられます。
Pullは、クライアントプログラムで一定時間ごとにデータを取得し、データが変わっているかどうかを判定します。主にポーリングと呼ばれます。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 4Oracleパッチが当たっていないと解除できないらしい。
https://stackoverflow.com/questions/46831869/delete-oracle-change-notificationsREVOKE CHANGE NOTIFICATION FROM HOGE_SCHEMA;権限をはく奪すれば、無事全部消えました。セーフ。
- 投稿日:2019-04-11T13:23:55+09:00
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.jsconst 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と打てば起動するはずです。
初めての投稿だったのでガバガバなところが多いと思いますが、暖かい目で見守ってください。
それでは。
- 投稿日:2019-04-11T09:50:55+09:00
GraphQLでスキーマ駆動開発導入したら開発効率がアップするぞ!!
現在携わっているプロジェクトに
GraphQLを使ってスキーマ駆動開発を導入したんですけど、かなり開発効率もよくなおかつ品質向上に良いのでやったことのまとめとして記事にしてみます。これからスキーマ駆動開発を導入を検討している方の後押しになれば幸いです。
スキーマ駆動以前の開発だと何が辛いのか
どうしてもフロントエンド開発の着手が遅くなりがちという点があると思います。
基本的には設計 > API実装 > フロント実装という流れなんですよね。さらに開発を進めていく中でうまく行かない部分があればAPIの実装・修正が必要になる場合もあります。そうするとさらにフロントの実装は遅れてしまいます。あとはAPIの実装がこんな感じになりそうだから、そうなるていでフロントも実装したりも。んで、あとで出戻りがあったりして開発効率としては良くないですね。
スキーマ駆動開発だと何が嬉しいのか
- API開発とフロント開発を同時に進めることができる
- スキーマがドキュメントとして存在するので出戻りがない(少ない)
- スキーマを元に型定義するので品質もアップ
あとは直接スキーマ駆動開発の利点ではないのですが、後述の graphql-codegen を使うとコマンド一つでスキーマから型定義ファイルを生成することができ、さらに開発効率がアップします。
開発手順のおさらい
まずスキーマ駆動の開発手順としてはざっくり以下のよう流れになっています。
- スキーマの定義
- mock作成
- フロント、サーバ共に実装
- テスト or リリース
以下、開発手順と照らし合わせながら記事を進めます。
プロジェクトの構成図
基本方針として以下のことが決まっていました。
- GraphQLを使う
- サーバサイド(BFF)はnodeで
- フロントはReact
- すでにマイクロサービスはある
スキーマ
まずはスキーマの定義を行います。
例えばこんな感じで書きます。今回はわかりやすくUser一覧とUser単体の取得系とログインするスキーマを定義してみます。schema.graphqlscalar 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.graphqlquery user($id: ID!) { user(id: $id) { id name profileImageUrl registerDate } } query users { users { id name registerDate profileImageUrl } }login.graphqlmutation 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.ymloverwrite: 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: falseserver/src/gen/types.ts
かなり省略してるけど概ねこんな感じで生成されます。
types.tsexport 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.tstype 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-codegenでuseUsersQueryやuseLoginMutationを生成してくれるのでフロントでは使うだけです。この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なのかな、エラーハンドリングも
UNAUTHENTICATEDやINTERNAL_SERVER_ERRORのコードを返してくれるので、Toastなんかと組み合わせてそこまでコストかからずに実装できます。テストについて
基本的にスキーマから生成しているものなので、型やデータの整合性は取れています。さらにGraphQLエラーとしてValidationもしてくれるので、従来の開発スタイルよりもかなり品質は向上しますね。
ただ、ログインのMuationのようにemailやpasswordをvariablesとして渡す必要がある場合にはスキーマの変更がフロントまで行き届かないです。
例えばスキーマで
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の事例がもっと増えてくれると良いなと。
- 投稿日:2019-04-11T00:09:05+09:00
Python使わないけどPythonが欲しい(Ubuntu)
Ubuntu環境(Ubuntu Server 18.04.2 LTS) をインストールして、node環境を構築し、いざnpmでパッケージをインストールしようとすると、
「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



