https://a.storyblok.com/f/270183/327782/2fe77461dd/websockets-adventure.png

ブラウザでSMS:ウェブソケットの冒険

最終更新日 May 11, 2021

所要時間:7 分

Nexmo Messages APIによって行われる重要な作業の多くはサーバーサイドで行われます。メッセージの送信、受信、処理などが行われますが、通常、APIを利用する多くのアプリケーションのエンドユーザーからは、このアクティビティはすべて隠されています。

ほとんどの場合、エンドユーザーへのこれらのメッセージの表示は、それぞれのアプリケーションによって処理される。ブラウザでこれらのメッセージを表示するオプションもあるが、そのためにはサーバーからブラウザにメッセージをプッシュする何らかの方法が必要になる。そのためには、サーバーからブラウザにメッセージをプッシュする方法が必要になる。

これはかなり長いチュートリアルで、HTML、CSS、Javascript、および Node.js.すでに何が何だかわかっている場合は、どのセクションも自由に読み飛ばしてください。

SMSを送受信できる "仮想 "電話をブラウザ上で構築する手順を説明します。使用するのは Glitchを使ってアプリケーションをホストするので、セットアップ手順の一部はGlitch向けです。

Glitchは、開発者がサーバーのセットアップに煩わされることなく、アプリのビルドとデプロイをすぐに実行できるオンライン開発者環境です。このプラットフォーム上のアプリはすべてリミックスやパーソナライズが可能で、コードを共有し、仕組みを理解するのに最適な場所となっている。

また、Node.jsがインストールされている環境であれば、どのような環境でも使うことができる。このプロジェクトの依存関係はすべて npm.

初期設定と構成

GlitchでKoa.jsアプリを始める

Glitchは常にインターフェースと機能を改善しています。 サインインボタンをクリックし、GitHubまたはFacebook経由でログインするか、メールアドレスでサインアップしてください。

Glitch sign inGlitch sign in

その後 新規プロジェクトボタンをクリックする。3つの選択肢があります、 ホームページ, ハローエクスプレスそして hello-sqlite.このチュートリアルでは hello-expressを使用することで、Node.jsとnpmがすでにインストールされた環境が得られるからだ。

Glitch hello-expressGlitch hello-express

追加のノードパッケージをインストールするには、コマンドラインから コンソールボタンをクリックします。

ConsoleConsole

ログウィンドウを切り替えるには ログボタンをクリックして切り替えることができる。そこから、bash環境で標準的なCLIコマンドをすべて使うことができる。唯一の違いは、Glitchでは pnpmの代わりに npm.

グリッチの使用 エクスプレスをデフォルトのNode.jsフレームワークとして使用していますが、アプリを Koa.jsへの変換はそれほど複雑ではありません。Koaは、ES2015と非同期関数をサポートするために、node v7.6.0以上が必要であることに注意してください。

DependenciesDependencies

削除 expressそして body-parserをプロジェクトから削除する:

pnpm uninstall express body-parser

以下のコマンドでKoa.jsとkoa-bodyparserをインストールする:

pnpm install koa koa-bodyparser --save

コンソールとエディターは自動的に同期しないので、次のコマンドを実行してください。 refreshコマンドを実行して package.jsonコマンドを実行してください。

KoaKoa

また、アプリケーションのステータスがエラーを表示していることにも気づくだろう。デフォルトの server.jsファイルがまだ express.

これを修正するには server.jsの内容を次のコードに置き換える:

const Koa = require('koa');
const app = new Koa();

app.use(async ctx => {
  ctx.body = 'Hello Dinosaur';
});

app.listen(3000);

アプリケーションを表示しようとすると、空白のページが表示されます。 こんにちは、恐竜.

Koa.jsで静的ファイルを扱う

私たちは、ユーザーが受信者の電話番号やメッセージのようなSMSを送信するための関連情報を入力できるように、入力フィールドを備えた基本的なHTMLページを提供する必要があります。

koa-staticはKoa用の静的ファイルサービングミドルウェアです。以下のコマンドでコンソールからプロジェクトにインストールしてください:

pnpm install koa-static --save

物事を複雑にしないために、ランディングページに必要なすべてのファイルを publicフォルダに配置します。ファイルを index.htmlファイルを viewsフォルダから publicフォルダに移動させることができます。

Rename fileRename file

コンソールからコマンドラインで行うこともできる。

それが終わったら server.jsファイルを修正する。 publicフォルダから以下のようにファイルを配信する:

const serve = require('koa-static');
const Koa = require('koa');
const app = new Koa();

app.use(serve('./public'));

app.listen(3000);

console.log('listening on port 3000');

の代わりに hello worldの代わりに、アプリはデフォルトのGlitch index.htmlファイルを提供しなければならない。このファイルは、チュートリアルの後半で電話のインターフェイスを模倣するように変更します。

Nexmo APIを使い始める

DashboardDashboard

Glitchアプリに戻って Node.js用Nexmo REST APIクライアントをインストールする:

pnpm install nexmo@beta --save

Messages APIは現在まだベータ版なので、ベータ版を使用しています。プロジェクトを更新すると package.jsonはこのようになっているはずです:

Nexmo nodeNexmo node

これで基本的なセットアップは完了です。これはNexmoのAPIを利用したKoaアプリケーションの出発点となるもので、このプロジェクトをGlitchのスターターキットにして、そこからリミックスすることができます。

ウェブソケットとは?

Webソケットプロトコル WebSocketプロトコルは、クライアントとリモートホスト間の双方向通信を可能にするために、インターネット・エンジニアリング・タスク・フォース(IETF)によって開発された。

通信プロトコルの一種で HTTP(ハイパーテキスト転送プロトコル), FTP(ファイル転送プロトコル)または SMTP(簡易メール転送プロトコル).通信プロトコルは、マシン同士の通信を可能にする。ほとんどの通信プロトコルは、インターネットを介して2つのマシン間のオープンな接続を維持します。

ウェブが動作するHTTPはそれとは異なる。HTTPはリクエスト/レスポンス・モードで動作するため、コネクションレス・プロトコルとして知られている。ウェブ・ブラウザは画像、フォント、コンテンツなどをサーバーに要求しますが、要求が満たされると、ブラウザとサーバー間の接続は切断されます。

HTTP upgradeヘッダーを使用して、既存のHTTP接続を互換性のない新しいプロトコルにアップグレードすることが可能です。接続リクエストは 常に接続リクエストは常にクライアントによって開始されます。 426 Upgrade RequiredHTTP レスポンスをクライアントに返すことでアップグレードを強制することができます。

HTTP/1.1 接続は、TLS 接続(推奨される方法ではありません)、HTTP/2 接続、またはアップグレードの最も一般的な使用例である WebSocket 接続にアップグレードすることができます。クライアントとサーバーの間で WebSocket 接続を確立するには、アップグレード・ヘッダーを含む最初の HTTP/1.1 リクエストが必要です。

最初のヘッダーを簡略化すると次のようになる:

GET /chat HTTP/1.1
Host: server.example.com
Origin: http://example.com
Upgrade: websocket
Connection: Upgrade

サーバーがWebSocketプロトコルをサポートしていれば、アップグレード要求に同意し、ハンドシェイクに対して 101ステータスコードを返します:

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade

以外の場合は 101以外の場合は、WebSocket ハンドシェイクが完了していないことを示します。ハンドシェイクが完了すると、最初の HTTP/1.1 接続は WebSocket 接続に切り替わり、2 台のマシン間でデータを送受信できるようになります。

ソケットAPI WebSockets APIは、ウェブアプリケーションで 使用するWebSocketプロトコルを使用することができるインターフェースである。2012年以降にリリースされたほとんどのブラウザがWebSocket APIをサポートしているため、サポートはそれなりに充実している。

WebSocket オブジェクトは、サーバーへの WebSocket 接続の作成と管理、および接続上のデータ送受信のための API を提供します。新しい WebSocket オブジェクトは WebSocket コンストラクタで作成できます、 WebSocket(url[, protocols]).WebSocket コンストラクタに渡す URL は wsまたは wssスキームでなければなりません。

const socket = new WebSocket('ws://localhost:8080')

WebSocketオブジェクトは4つのイベントをディスパッチする:

  • open

  • error

  • message

  • close

WebSocket接続が確立されると openイベントが発生します。先ほどのコードのブロックで作成した WebSocket オブジェクトを参照すると、対応するイベントハンドラは次のようになります:

socket.onopen = function(evt) {
  // do something
}

デバッグのために errorイベントを使って、デバッグのためにエラーをコンソールに記録することができる:

socket.onerror = function(evt) {
  console.log('WebSocket error: ', evt)
  // include an error event handler here
}

最も一般的なのは messageイベントを利用する。例えば、サーバーからテキストメッセージが送信された場合、次のようにコンソールにログを記録することができる:

socket.onmessage = function(evt) {
  console.log('Message: ', evt.data)
}

最後に closeイベントが発生します。このイベントが発生すると、新しい接続が確立されるまで、クライアントとサーバーは互いにメッセージを送信できなくなります。

socket.onclose = function(evt) {
  console.log('WebSocket connection closed. ', evt)
}

また、WebSocketオブジェクトには2つのメソッドがあります、 send()サーバーにデータを送信するメソッドと close()既存の WebSocket 接続を終了するためのメソッドがあります。

この send()メソッドは、接続が開いている間だけ動作する。これは当然のことのように思えるが、コードの構造を間違えると、接続が開く前、あるいは接続が閉じた後にトリガーされてしまう可能性がある。

// Listen for the open event before triggering the sendMsg() function
socket.onopen = function(evt) {
  console.log('Connection established')
  sendMsg('Hello Nexmo!')
}

// Send a message through the WebSocket connection
function sendMsg(data) {
  socket.send(data)
}

WebSocketとは何か、WebSocket APIがどのように動作するかについて理解を深めたところで、この知識を活用して、ブラウザ経由で直接SMSを送受信する基本的なアプリケーションを作成する準備が整いました。 NexmoメッセージAPI.

NexmoでブラウザからSMSを受信する

バーチャル電話番号の取得

Messages APIを使ってSMSを送受信するには、次のものが必要です。 仮想電話番号通常の電話番号と同じですが、物理的な電話回線やデバイスに縛られることはありません。

Numbersからバーチャル番号を購入することができます。 Numbersセクションで Numbersを購入する.ご希望の国の現地番号、サポートする機能、携帯電話、固定電話、フリーダイヤルなどの番号の種類を選択できます。

Buy numbersBuy numbers

マイナンバーが発行されると、以下のページに表示されます。 あなたのNumbersセクションに表示されます。右端にある鉛筆のアイコンをクリックしてください。 管理列の下にある鉛筆のアイコンをクリックして、受信ウェブフックURLを設定します。これはSMSを受信するために必要です。SMSがあなたの番号に送信されると、このURLにメッセージのペイロードと共にリクエストが送信されます。 POSTリクエストがメッセージのペイロードと共にこのURLに送信されます。

Inbound webhook URLInbound webhook URL

Glitchを使用している場合、ウェブフックのURLは次のようになります。 https://boom-meal.glitch.me/inbound-sms.ルートとして inbound-smsをルートとして使用することをお勧めしますが、そのルートがあなたのアプリケーションで有効である限り、お好きなものを使用できます。WebhookのURLは、アプリケーションがどこでホストされているかによって適宜調整してください。

もうひとつ確認すべきことは、あなたのアカウントのデフォルトのSMS設定が、HTTPメソッドで POSTに設定されていることです。

SettingsSettings

インバウンドSMSの受信

着信SMSを受信するには、アプリケーションにルートを追加して、誰かがあなたの仮想番号にSMSを送信したときに発生する着信リクエストを処理する必要があります。 POSTリクエストを処理するルートをアプリケーションに追加する必要があります。ExpressやHapi.jsのような他の一般的なNode.jsフレームワークとは異なり、ルーティングは別のモジュールによって処理されるので、最初にそれをインストールする必要があります。

pnpm install koa-router --save

あなたの server.jsファイルに追加する:

const bodyParser = require('koa-bodyparser');
const Router = require('koa-router');

const router = new Router();

app.use(bodyParser());

// Make sure this is the route you configured for your virtual number
router.post('/inbound-sms', async (ctx, next) => {
  const payload = ctx.request.body;
  console.log(payload);
  ctx.status = 200;
});

app.use(router.routes()).use(router.allowedMethods());

このチュートリアルの前の方のセットアップ手順に従ったのであれば koa-bodyparserがインストールされているはずです。リクエストボディを処理するには、ボディパーサーが必要です。すべてが正しく接続されていることを確認するには、あなたのバーチャル番号にSMSを送信する。メッセージのペイロードがコンソールに記録されるはずです。

SMS payloadSMS payload

WebSocketサーバーの作成

これで動作が確認できたので、コンソールにメッセージのペイロードを記録する代わりに、WebSocketを使って関連情報をブラウザに送信してみよう。

その前に、Node.jsのWebSocketヘルパー・ライブラリーである ws.

pnpm install ws --save

これが終わったら wsserver.jsファイルを以下のように使用することで、ウェブ・ソケット・サーバーを作成することができる:

const http = require('http');
const WebSocket = require('ws');

// Create a web socket server on top of a regular http server
const server = http.createServer(app.callback());
const wsserver = new WebSocket.Server({ server: server });

オリジナルを app.listen(3000)server.listen(3000).

さらに、受信メッセージのペイロードを処理する関数を追加し、このデータをブラウザに送信してみよう。

function receiveSms(payload) {
  const phone = payload.msisdn;
  const msg = payload.text;
  const timestamp = payload['message-timestamp'];
  
  const smsData = JSON.stringify({ 
    phone: phone,
    msg: msg,
    timestamp: timestamp
  });

  // Broadcast the message to all open clients
  wsserver.clients.forEach(client => {
    if (client.readyState === WebSocket.OPEN) {
      client.send(smsData);
    }
  });
}

チュートリアルのこの時点までの server.jsチュートリアルのここまでのファイル全体は次のようになるはずだ:

const Koa = require('koa');
const bodyParser = require('koa-bodyparser');
const Router = require('koa-router');
const serve = require('koa-static');

const http = require('http');
const WebSocket = require('ws');

const app = new Koa();
const router = new Router();
const server = http.createServer(app.callback());
const wsserver = new WebSocket.Server({ server: server });

app.use(serve('./public'));
app.use(bodyParser());

router.post('/inbound-sms', async (ctx, next) => {
  const payload = ctx.request.body;
  receiveSms(payload);
  ctx.status = 200;
});

app.use(router.routes()).use(router.allowedMethods());

server.listen(3000);
console.log('listening on port 3000');

function receiveSms(payload) {
  const phone = payload.msisdn;
  const msg = payload.text;
  const timestamp = payload['message-timestamp'];
  
  const smsData = JSON.stringify({ 
    phone: phone,
    msg: msg,
    timestamp: timestamp
  });

  // Broadcast the message to all open clients
  wsserver.clients.forEach(client => {
    if (client.readyState === WebSocket.OPEN) {
      client.send(smsData);
    }
  });
}

ブラウザで受信SMSを表示する

次のステップは、ブラウザがサーバーからこのデータを受け取る準備ができていることを確認することだ。

グリッチが提供する client.jsファイルのデフォルトの内容を消去し、その内容を以下のように置き換える:

document.addEventListener("DOMContentLoaded", function() {
  let socket = null;
  let connected = false;
  
  function start() {
    socket = new WebSocket('wss://' + location.host);
    
    socket.onopen = function(evt) {
      connected = true;
      console.log('open')
    }
    
    socket.onmessage = function(evt) {
      const reply = JSON.parse(evt.data);
      console.log(reply);
      addMsg(reply.phone, reply.msg, reply.timestamp, 'messages');
    }
    
    socket.onclose = function(evt) {
      connected = false;
      console.log('closed')
      check();
    }
  }

  function check() {
    if(!socket || socket.readyState == 3) start();
  }

  start();
  setInterval(check, 5000);
});

ここには2つの関数がある、 start()新しい WebSocket 接続と対応するイベントハンドラを確立する関数と check()接続の状態をチェックし、切断された場合は再接続します。

さて、アプリケーションのブラウザ・コンソールを開いてしばらく放っておくと、一連の opencloseがコンソールに記録されているはずです。

CheckCheck

要素のデフォルトのマークアップを index.htmlファイルのデフォルトのマークアップを次のように置き換える:

<main>
  <h1>V-mobile</h1>
  <ul id="messages"></ul>
</main>

もっと携帯電話らしく見せるための要素やスタイルは、後で追加できる。とりあえず、まずはすべてのデータを正しい場所に表示できるようにしよう。さらに2つの関数を client.jsファイルに、サーバーから送られてきたデータを解析してDOMに追加する関数を2つ追加します。

function addMsg(sender, content, time, parent) {
  const messages = document.getElementById(parent); 
  const newMsg = document.createElement('li');
  messages.appendChild(newMsg);
  
  appendData(newMsg, sender);
  appendData(newMsg, content);
  appendData(newMsg, time);
}

function appendData(newMsg, data) {
  const textSpan = document.createElement('span');
  const textContent = document.createTextNode(data);
  textSpan.appendChild(textContent);
  newMsg.appendChild(textSpan);
}

この新しい関数を onmessageイベント・ハンドラにこの新しい関数を追加すると、バーチャル番号にSMSを送信したときに、そのSMSがページに表示されるようになります。

socket.onmessage = function(evt) {
  const reply = JSON.parse(evt.data);
  console.log(reply);
  addMsg(reply.phone, reply.msg, reply.timestamp, 'messages');
}

First InboundFirst Inbound

NexmoのMessages APIを使ってブラウザからSMSを送信する

メッセージアプリケーションの作成

次に、Nexmoのダッシュボードに戻り、「アプリケーションの作成」に移動します。 アプリケーションの作成ページに移動します。 メッセージとディスパッチセクションに移動します。アプリケーション名を入力し、GlitchアプリのURLをホストとしてウェブフックのURLを入力します。また、公開鍵と秘密鍵のペアを生成する必要があります。 private.keyファイルをダウンロードするよう促されます。

Generate keyGenerate key

ウェブフックURLの両方が設定されていること、そしてアプリケーションがこれらのエンドポイントの両方に対してリクエストを受け付けるルートを設定していることが重要です。 POSTリクエストを受け入れるためのルートが設定されていることが重要です。

次に、オレンジ色の アプリケーションの作成ボタンをクリックします。次の画面では、バーチャル・ナンバーと申請書をリンクさせることができます。 リンクボタンをクリックします。 管理ボタンをクリックします。

Create a message applicationCreate a message application

最後に、外部アカウントをリンクするかどうか尋ねられますが、今はそのままにしておいてください。

External accountsExternal accounts

ファイルを private.keyファイルをGlitchにアップロードし、それを秘密にするには、ファイルを .dataフォルダにファイルを作成することができます。このフォルダの内容は、あなたと、あなたがプロジェクトに追加した信頼できる共同作業者のみが見ることができます。先ほどダウンロードした private.keyの内容をこの新しいファイルにコピーしてください。

Private keyPrivate key

認証情報の設定

グリッチは 環境変数をサポートしている。 .envファイルを通して環境変数をサポートしています。これは、API認証情報やプロジェクトのその他のプライベートデータを安全に保存する方法です。APIキー、シークレット、Nexmo仮想番号、MessagesアプリケーションID、秘密鍵パスを .envファイルに設定します。

それぞれの値は文字列である必要があるので、必ず引用符で囲んでください。SMSメッセージの送信に使用する新しいNexmoインスタンスを初期化するために、これらの値を参照します。

env fileenv file

API認証情報を server.jsファイルに追加し、新しいNexmoインスタンスを初期化します。

const NEXMO_API_KEY = process.env.NEXMO_API_KEY;
const NEXMO_API_SECRET = process.env.NEXMO_API_SECRET;
const NEXMO_APPLICATION_ID = process.env.NEXMO_APPLICATION_ID;
const NEXMO_APPLICATION_PRIVATE_KEY_PATH = process.env.NEXMO_APPLICATION_PRIVATE_KEY_PATH;
const NEXMO_NUMBER = process.env.NEXMO_NUMBER;

const Nexmo = require('nexmo');

const nexmo = new Nexmo({
  apiKey: NEXMO_API_KEY,
  apiSecret: NEXMO_API_SECRET,
  applicationId: NEXMO_APPLICATION_ID,
  privateKey: NEXMO_APPLICATION_PRIVATE_KEY_PATH
});

ブラウザからSMSを送信する

ユーザーが受信者の電話番号と送信したいメッセージを入力する方法が必要なので、そのための入力フィールドをいくつか追加しよう。

<input type="tel" name="phone" placeholder="Send SMS to…?" required>
<input type="text" name="msg">
<button id="send" type="button">Send</button>

InputsInputs

ユーザー入力を処理するには、以下を client.jsファイルに追加する:

const msgField = document.querySelector('input[name="msg"]');
const phoneField = document.querySelector('input[name="phone"]');
const sendBtn = document.getElementById('send');

msgField.addEventListener('keypress', (evt) => {
  if (evt.keyCode === 13) {
    const passed = phoneCheck(phoneField);

    if (passed) {
      send(socket, phoneField, msgField);
    } else {
      console.log('Phone field was not filled. To do: add error handling UI here.');
    }
  }
}, false);

sendBtn.addEventListener('click', (evt) => {
  const passed = phoneCheck(phoneField);

  if (passed) {
    send(socket, phoneField, msgField);
  } else {
    console.log('Phone field was not filled. To do: add error handling UI here.');
  }
}, false);

function phoneCheck(phoneField) {
  return phoneField.value !== '' && /^\d+$/.test(phoneField.value);
}

function send(socket, phoneField, msgField) {
  const phone = phoneField.value.replace(/\D/g,'');
  const msg = msgField.value;
  const payload = JSON.stringify({
    phone: phone,
    message: msg
  });

  socket.send(payload);

  const date = new Date();
  const time = date.toLocaleTimeString();

  addMsg('YOUR_APP_NAME', msg, time, 'messages');
  msgField.value= '';
}

ここでやっているのは、ユーザーがクリックしたときに send()関数をトリガすることです。 送信ボタンまたは Enterキーをクリックしたときに関数を起動することです。また、電話番号フィールドには非常に初歩的なチェック機能があり、数字のみが入力されていることを確認します。メッセージは、一般的なメッセージング・アプリケーションのように、ブラウザ上にも表示されます。

たくさんある。 セキュリティとUXの観点からセキュリティとUXの観点から、フィールドのバリデーションにはもっとやるべきことがたくさんあり、このチュートリアルの範囲外ですが、バリデーションは重要であり、それが正しく行われることを確認するために労力とテストを費やすことは常にベストプラクティスです。

サーバー側では、ブラウザから送られてきたデータを受信者の電話番号に転送する必要がある。

以下のルートを server.jsファイルに追加する:

router.post('/webhooks/inbound-message', async (ctx, next) => {
  ctx.status = 200;
});

router.post('/webhooks/message-status', async (ctx, next) => {
  ctx.status = 200;
});

これは POSTこれは、Nexmoからのリクエストが適切なエンドポイントに送信され、正常に配信されたときにレスポンスを受け取れるようにするためである。 200レスポンスを受け取れるようにするためです。

メッセージ・ハンドラと forwardSMS()関数を追加します。この関数はNexmoのMessages APIを利用し、ブラウザからのデータを受信したら、対象の受信者にSMSを送信します。

wsserver.on('connection', function connection(socket) {
  socket.on('message', data => {
    forwardSms(JSON.parse(data));
  });
});

function forwardSms(payload) {
  const phone = payload.phone;
  const msg = payload.message;

  nexmo.channel.send(
    { "type": "sms", "number": phone },
    { "type": "sms", "number": NEXMO_NUMBER },
    {
      "content": {
        "type": "text",
        "text": msg
      }
    },
    (err, data) => {
      if (err) { console.log(err) }
      if (data) { console.log('Outbound message successful: ', data.message_uuid) }
    }
  )
}

すべてが期待通りに動作することをテストするには、アクセス可能な携帯電話の電話番号を入力し、テストメッセージを送信します。対象の受信者は数秒以内にSMSを受信するはずです。

Forward SMSForward SMS

対象の受信者がSMSスレッドに返信した場合、メッセージはブラウザに送り返されます。

ReplyReply

基礎となるシステムが稼働し始めた今、このプロジェクトを続けたいのであれば、インターフェイスをより美しくしたり、クライアントとサーバーの両方でより強力なバリデーション・チェックを追加したり、エッジケースを処理したりなど、できることがいくつかある。

このプロジェクトの私のバージョンはV-mobileと呼ばれている。 グリッチ.もし気に入ったら、僕のプロジェクトを自由にリミックスして自分のものにしてほしい。

End resultEnd result

次はどこだ?

これらのAPIをもっと使いたいとお考えなら、以下のリンクが参考になるでしょう:

シェア:

https://a.storyblok.com/f/270183/384x384/46621147f0/huijing.png
Hui Jing Chenヴォネージの卒業生

ホイ・ジンはNexmoのデベロッパー・アドボケイト。CSSとタイポグラフィをこよなく愛する。