https://a.storyblok.com/f/270183/62751/f86b373273/elevate_conversationvuejs-1.png

Vue.jsとExpressでVoiceチャットアプリケーションを作成する

最終更新日 April 30, 2021

所要時間:2 分

複雑なウェブアプリケーションにサービスを追加することは、保守可能な方法で行うには厄介なことです。サービスにユーザーインターフェイスのコンポーネントがある場合はなおさらです。NexmoのAPIを使えば、様々なコミュニケーション・アプリケーションの基礎となるブラウザ内ボイスチャットを作ることができます。しかし、その基本的なUIの断片を整理することさえ難しい。コンポーネント Vue.jsのようなコンポーネントは、個々のUIコンポーネントが必要とするテンプレート、スタイリング、UIスクリプトのパターンを提供することで、これを容易にします。Nexmoのツールに接続するExpressサーバーは、関心事の分離のおかげで、どのような実世界のアーキテクチャにも適応できる軽量のフルスタック・ソリューションを提供します。

Vueでアプリケーションを構成する方法はたくさんあります。このチュートリアルでは Glitchプロジェクトをリミックスします。 Vue CLIまたは サードパーティライブラリが提供するスターター・プロジェクトや、サーバーサイド・レンダリングのような特定の機能を提供するサードパーティのライブラリを選択することもできます。あなたのコードはVueとExpressの両方に依存するため、セットアップに両方が含まれていることが唯一の要件です。

プロジェクトにNexmoを追加する

ブラウザから会話を作成するには、Nexmoの クライアントサーバーパッケージの両方をインストールする必要があります。ユーザはクライアントからデータを送信しますので body-parserも必要です。プロジェクトのルート・ディレクトリに、これらのパッケージをインストールしてください。 npmまたは、Glitchのコンソールから pnpm:

pnpm install nexmo@beta nexmo-client body-parser -s

Nexmoのツールを使うには、API認証情報を .envファイルで提供する必要があります。ファイルは以下のようなものです:

API_KEY="12ab3456" API_SECRET="123AbcdefghIJklM" APP_ID="a0b23456-c789-012d-3456-e789012f34a5" PRIVATE_KEY="/.data/private.key"

環境によっては、npmから dotenvパッケージをインストールする必要があるかもしれません。から環境変数をインポートするには .envから環境変数をインポートするには、次の行を server.jsファイルの先頭に1行追加するだけです: require('dotenv').config();

APIキーとシークレットは 入門ページにあります。 にあります。.Voiceメニューの下にある アプリケーションの作成をクリックし、"Generate public/private key pair "をクリックしてファイルをダウンロードします。 private.keyファイルをダウンロードします。その後、フィールドに必要事項を入力し、「アプリケーションを作成」をクリックしてアプリケーションIDを取得します。

必ず private.keyファイルをプロジェクトにコピーし、パスを .envを更新してください。に直接ペーストすることも可能です。 .envに直接貼り付けることも可能ですが、書式に問題が生じる可能性があります。一般的には、別のファイルに保存する方がより堅牢です。

APIコール用サーバー

の役割 Express.jsの役割は、Nexmo APIを呼び出していくつかの管理タスクを実行するシンプルなサーバーを提供することです。そのためには、サーバー自体のセットアップ、Nexmoインスタンス、サーバーエンドポイントのルート定義が必要になります。

server.jsで、サーバーを作成し、リクエスト・ボディのJSONを解析し、静的ページを publicディレクトリから静的ページを提供する。次に、Nexmoオブジェクトを作成し、そのオブジェクトに .env.最後に、ルート用のプレースホルダを作成し、サーバーにイベントのリッスンを開始するように指示します:

const express = require('express');
const app = express();
const bodyParser = require('body-parser');
app.use(bodyParser.json());
app.use(express.static('public'));

// create a Nexmo client
const Nexmo = require('nexmo');
const nexmo = new Nexmo({
  apiKey: process.env.API_KEY,
  apiSecret: process.env.API_SECRET,
  applicationId: process.env.APP_ID,
  privateKey: __dirname + process.env.PRIVATE_KEY 
}, {debug: true});

// the client calls this endpoint to request a JWT, passing it a username
app.post('/getJWT', function(req, res) {});

// the client calls this endpoint to get a list of all users in the Nexmo application
app.get('/getUsers', function(req, res) {});

// the client calls this endpoint to create a new user in the Nexmo application,
// passing it a username and optional display name
app.post('/createUser', function(req, res) {});

app.listen(process.env.PORT);

サーバー・ルート

サーバー上で定義された3つのルートは、アプリケーションが会話に参加できるユーザーをリストアップし、作成し、認証することを可能にします。実世界で使われるアプリケーションでは、おそらくウェブインターフェースの代わりに、独自のユーザー管理にこれを接続するでしょう。

ルート /getJWTは、クライアントが現在のユーザーを認証するためのトークンを提供します。この JWTの生成は1つの関数で行われますが、いくつかのデータが必要です。アプリケーションIDと、認証したいユーザー名である subこれは認証したいユーザー名である。また、トークンの有効期限と許可パスも設定します。新しく作成したトークンをクライアントに送ることができます:

// the client calls this endpoint to request a JWT, passing it a username
app.post('/getJWT', function(req, res) {
  const jwt = nexmo.generateJwt({
    application_id: process.env.APP_ID,
    sub: req.body.name,
    exp: Math.round(new Date().getTime()/1000)+3600,
    acl: {
      "paths": {
        "/v1/users/**":{},
        "/v1/conversations/**":{},
        "/v1/sessions/**":{},
        "/v1/devices/**":{},
        "/v1/image/**":{},
        "/v3/media/**":{},
        "/v1/push/**":{},
        "/v1/knocking/**":{}
      }
    }
  });
  res.send({jwt: jwt});
});

この /getUsersパスも1回の呼び出しで結果を返しますが、ウェブ・インターフェイスで使用するために少し整理してみましょう。このアプリケーションの全ユーザーのリストを返す前に、IDがプレフィックス NAM-.ユーザーIDが大規模なアプリケーション内のアカウントにマッピングされているような現実世界のアプリケーションでは、おそらくこのステップに悩まされることはなく、そのままリストを返すことができるでしょう:

// the client calls this endpoint to get a list of all users in the Nexmo application
app.get('/getUsers', function(req, res) {
  const users = nexmo.users.get({}, (err, response) => {
    if (err) {
      res.sendStatus(500);
    } else {
      let realUsers = response.filter(user => user.name.substring(0,4) !== 'NAM-');
      res.send({users: realUsers});
    }
  });
});

最後のルート /createUserはユーザーの入力を受け取り、アプリケーションにユーザーを追加します。なぜなら create関数はユーザー名と表示名の両方を入力として受け取るので、このコードでは表示名を別に設定するオプションがあります。したがって、エンドポイントはクライアントから nameを探し、それでユーザーを作成したら、そのIDを返します:

// the client calls this endpoint to create a new user in the Nexmo application,
// passing it a username and optional display name
app.post('/createUser', function(req, res) {
  nexmo.users.create({
    name: req.body.name,
    display_name: req.body.display_name || req.body.name
  },(err, response) => {
    if (err) {
      res.sendStatus(500);
    } else {
      res.send({id: response.id});
    }
  });
});

Vueアプリ・コンポーネント

このプロジェクトのVueコンポーネントはすべて srcディレクトリにあります。今回リミックスするプロジェクトには、すでに main.jsファイルが含まれています。 app.vue. main.jsのコンテナ・コンポーネントは、Appコンポーネントをレンダリングする以外のことは何もしません:

var Vue = require('vue');
var App = require('./app.vue');

var vm = new Vue({
  el: '#app',
  render: createElement => {
    return createElement(App)
  }
});

これは public/index.htmlと協調して動作します。 appを持つdivがページ上の唯一の要素である場合、これは

<!DOCTYPE html>
<html>
<head>
  <title>VueJS + Express Template</title>
  <meta name="viewport" content="width=device-width,initial-scale=1">
</head>
<body>
  <div id="app"></div>
  <script src="build.js"></script>
</body>
</html>

後でさらに追加したい場合に備えて Appコンポーネントを置き、その中に Nexmoコンポーネントを Appで置き換えるのではなく Nexmo.すでに app.vueファイルの中身を、単純なテンプレートとスクリプトで置き換えることができます。 Nexmoコンポーネントに置き換えることができます:

template>
  <div class="app">
    <Nexmo/>
  </div>
</template>

<script>
import Nexmo from './nexmo.vue';

export default {
  name: 'App',
  components: {
    Nexmo
  }
}
</script>

ネクスモ・コンポーネント

コンポーネントは Nexmoコンポーネントが面白くなってくるところだ。このコンポーネントは nexmo.vueで作成し、ファイルの最初にテンプレートを追加する。 Userそして Conversationコンポーネントを作成します。例えば Userの場合、更新フックは次に追加するスクリプトの関数 getJWTを呼び出します。コンポーネントへの参照を追加して、後でアクセスすることもできます:

<template>
  <div class="nexmo">
    <User @hook:updated="userUpdated" ref="user" />
    <Conversation/>
  </div>
</template>

テンプレートの下に、コンポーネントのロジックを含むスクリプトタグを追加します。2つのサブコンポーネントとNexmo Client SDKをインポートしたら、Vueコンポーネントをエクスポートします。 Nexmo.このコンポーネントには dataプロパティ、サブコンポーネント、次に定義するメソッドが含まれます:

<script>
  import User from './user.vue';
  import Conversation from './conversation.vue';
  import nexmoClient from 'nexmo-client';
  
  export default {
    name: 'Nexmo',
    data: () => ({
      app: null,
      token: null,
      invites: [],
      loggedIn: false
    }),
    components: {
      User,
      Conversation
    },
    methods: {}
  };
</script>

この methodsプロパティは2つの関数を定義します。1つはサーバーからJWTを取得する関数、もう1つはログインを処理する関数です。この getJWT関数は Userコンポーネント上の更新フックによって呼び出されます。 usernameプロパティが含まれているかどうかをチェックする必要があります。もし含まれていれば、サーバー側の /getJWTエンドポイントを呼び出すことができます。 fetch.文字列化された username値を渡し、すべてがスムーズに動作すれば、JWTを返します。JWTをインスタンスのプロパティとして保存し、. login関数を呼び出す。

この login関数は実際のNexmoクライアントをインスタンス化する場所です。ユーザーのJWTでログインし、成功したらフラグを立ててNexmoアプリケーションへの参照を保存します。アプリケーションを取得すると、現在のユーザーが招待されている会話を取得できます:

    methods: {
      getJWT: function() {
        var username = this.$refs.user.username;
        if (!username) {
          return;
        }
        var vm = this;
        fetch('/getJWT', {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json'
          },
          body: JSON.stringify({
            name: username
          })
        })
        .then(results => results.json())
        .then(data => {
          vm.token = data.jwt;
          vm.login();
        });
      },
      login: function() {
        let nexmo = new nexmoClient();
        nexmo.login(this.token).then(app => {
          this.loggedIn = true;
          this.app = app;
          app.getConversations().then(convos => {
            this.invites = Array.from(convos.entries());
          });
        });
      }
    }

ユーザー・コンポーネント

その Userコンポーネント src/user.vueにあるコンポーネントは、サブコンポーネントのレンダリング以外に何かを行う最初のテンプレートです。これも本番アプリでは省略できる部分ですが、この場合、ユーザーログインインターフェースはNexmoアプリの一部です。このテンプレートは、接続しているユーザが存在すれば、そのユーザを表示します。存在しない場合は、2つのパスを持つフォームが表示されます。1つ目は、ドロップダウンから既存のユーザーを選択するものです。選択されると、そのユーザはすぐに setExistingUser関数によって更新されます。

ユーザーは新しいユーザー名を入力し、送信ボタンをクリックすることもできる。これは createUser関数を呼び出します:

<template>
  <div v-if="userId" class="userinfo userconnected">
    Connected as <span class="username">{{username}}</span>
  </div>
  <div v-else class="userinfo">
    <label>User name: 
      <select v-on:change="setExistingUser">
        <option value=""></option>
        <option v-for="item in currentUsers" v-bind:value="item.id">
          {{item.name}}
        </option>
      </select>
    </label>
    <input type="text" v-on:change="setUsername" />
    <button v-on:click="createUser">Create user</button>
  </div>
</template>

ユーザー・コンポーネント・スクリプト

コンポーネントの scriptにはいくつかの異なる処理がありますが、複雑なロジックはありません。やっていることのほとんどは、プロパティの読み込みと保存です。複雑な処理は、Vueフレームワーク自体で行われます。 Nexmoコンポーネントの更新フックのような機能で行われます。

インポートするものは何もない。 Userコンポーネントをエクスポートできます。必要なのは data必要なのは、ユーザーのIDと名前のプロパティと、アプリケーションの現在のユーザーのリストだけです。

このコンポーネントには、テンプレート内のフォームをサポートする4つのメソッドがあります。そのうちの getUsers関数は /getUsersを呼び出します。必要なフィルタリングロジックはサーバサイドで処理したので、エラーがなければコンポーネントにそのプロパティを設定するだけでよいことを覚えておいてください。

setExistingUseronchangeイベントによって呼び出されます。選択されたユーザー名とユーザーIDが保存されます。新規ユーザの場合 setUsernameによっても呼び出されます。 onchangeによって呼び出されます。新しいユーザ名が変更されるたびにコンポーネント上で更新することで、テキストフィールド要素への参照を取得する手間を省くことができます。ユーザが "Create user "ボタンをクリックすると、? createUserが呼び出され、ユーザ名の状態をサーバに送信し、返されたユーザIDを保存します。

後に methodsこのコンポーネントもまた beforeMountを呼び出して、最初に初期化されたときにユーザーのリストが読み込まれるようにしています:

<template>
  ...
</template>

<script>  
export default {
  name: 'User',
  data: () => ({
    userId: undefined,
    username: null,
    currentUsers: []
  }),
  methods: {
    getUsers: function() {
      var vm = this;
      fetch('/getUsers', {
        method: 'GET'
      }).then(results => results.json())
      .then(data => {
        vm.currentUsers = data.users;
      });
    },

    setExistingUser: function(evt) {
      this.username = evt.target[evt.target.selectedIndex].text;
      this.userId = evt.target.value;
    },

    setUsername: function(evt) {
      this.username = evt.target.value;
    },

    createUser: function() {
      var vm = this;
      fetch('/createUser', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({
          name: vm.username
        })
      }).then(results => results.json())
        .then(data => { 
          vm.userId = data.id;
        });
    }
  },
  beforeMount() {
    this.getUsers()
  }
};
</script>

カンバセーション・コンポーネント

今までのコードは、Nexmoアプリを作成し、ユーザーを設定し、ログインするためのものでした。Vueアプリに必要な情報を公開しつつ、個々のプロジェクトのニーズに合わせて変更できるように、コードを分離しておく必要があります。これらの情報を使って、会話に参加することができます。会話は、クライアント上でNexmoのAPIを使って行う様々なタイプのコミュニケーションの出発点となります。

このコンポーネントは、いくつかのサブコンポーネント(オーディオ・コントロールなど)を分割した方が管理しやすいだろう。しかし、よりわかりやすくするために、とりあえずすべてのコードを conversation.vue.

テンプレート

テンプレートのトップレベルには、進行中の会話が存在するかどうかを判断するための条件があります。存在する場合は、音声を供給するためのaudio要素と、音声を有効または無効にするための2つのボタンがレンダリングされます。クリックすると enableAudiodisableAudioを呼び出します。

現在会話がない場合、ユーザーは会話に参加するか開始する必要があります。ユーザが会話に招待されているか、以前に会話を開始している場合、その会話は親の invites配列に表示されます。 Nexmoコンポーネントの配列に表示されます。の値は invitesの値は会話のドロップダウンに入力され、1つを選択すると joinConversation関数を呼び出します。ユーザが既存の invitesに関わらず、新しい会話を開始するボタンが表示されます:

<template>
  <div v-if="current_conv" class="conversation">
    <audio ref="audio">
      <source/>
    </audio>
    <button v-on:click="enableAudio" v-bind:disabled="audioOn">
      Enable audio
    </button>
    <button v-on:click="disableAudio" v-bind:disabled="!audioOn">
      Disable audio
    </button>
  </div>
  <div v-else class="conversation">
    <label v-if="$parent.invites.length">Choose an active conversation: 
      <select v-on:change="joinConversation">
        <option value="0">-</option>
        <option v-for="invite in $parent.invites" v-bind:value="invite[0]">
          {{invite[1].name}}
        </option>
      </select> or
    </label>
    <button v-on:click="createConversation" :disabled="!$parent.loggedIn">
      Start conversation
    </button>
  </div>
</template>

スクリプト

このコンポーネントでも、トップ・レベルの scriptタグのトップ・レベルでは Conversationコンポーネントをエクスポートしている。その唯一の dataは、現在の会話と、オーディオが有効かどうかを追跡するフラグだけです。

コンポーネントに含まれる methodsコンポーネントが含むものは、すべて非常に単純なものである。 createConversation親に保存されているアプリの newConversationNexmoコンポーネントに保存されたアプリの関数を呼び出し、作成された会話を保存します。 joinConversationは、アプリの getConversation関数を呼び出し、ドロップダウンで選択された会話のIDを渡します。

enableAudioで、まず現在の会話でメディアを有効にする必要があります。そうすると、ストリームを srcObjectまたは src要素の audio要素に設定できます。メタデータがロードされたら、ストリームを再生し、コンポーネントの audioOnフラグを true.を設定します。 disableAudioDisable audio "ボタンがクリックされたときに呼び出される関数は、もっと単純です。でメディアを無効にし current_convでメディアを無効にし audioOnフラグを false:

<template>
  ...
</template>

<script>
  export default {
    name: 'Conversation',
    data: () => ({
      current_conv: undefined,
      audioOn: false
    }),
    methods: {
      createConversation: function() {
        var vm = this;
        this.$parent.app.newConversation().then(conv => {
          conv.join();
          vm.current_conv = conv;
        });
      },
  
      joinConversation: function(evt) {
        var vm = this;
        this.$parent.app.getConversation(evt.target.value).then(conv => {
          conv.join();
          vm.current_conv = conv;
        });
      },

      enableAudio: function() {
        var vm = this;
        this.current_conv.media.enable().then(stream => {
          // Older browsers may not have srcObject
          if ('srcObject' in vm.$refs.audio) {
            vm.$refs.audio.srcObject = stream;
          } else {
            // Avoid using this in new browsers, as it is going away.
            vm.$refs.audio.src = window.URL.createObjectURL(stream);
          }
          vm.$refs.audio.onloadedmetadata = () => {
            vm.$refs.audio.play();
            vm.audioOn = true;
          }
        });
      },

      disableAudio: function() {
        var vm = this;
        this.current_conv.media.disable().then(() => {
          vm.audioOn = false;
        });
      }
    }
  };
</script>

その他

Vueのボイラープレートから提供されていたり、必ずしもアプリケーションのロジックに影響を与える必要がなかったりすればいいのですが。例えば、私自身のコードでは ブラウザ化Vueifyに依存しています。アプリケーションのVue側を動作させるビルド・ステップは、以下の "scripts "で定義されている。 package.json:

"compile": "browserify -t vueify -e src/main.js -o public/build.js"

次のステップ

あなたが書いたコードは、実世界での作業のための出発点です。前述したように、おそらくテストユーザー管理システムを、会話メンバーとあなた自身の認証されたユーザーを結びつけるものに置き換えたくなるでしょう。会話を作成すると、メッセージの送受信、通話の受信、音声会議の設定ができるようになります。

NexmoクライアントSDKの詳細はこちらをご覧ください:

シェア:

https://a.storyblok.com/f/270183/250x250/f231d97f1b/garann-means.png
Garann Meansデベロッパー・エデュケーター

私はJavaScript開発者で、Vonageの開発者教育者です。長年にわたり、テンプレート、Node.js、プログレッシブ・ウェブ・アプリケーション、そしてオフライン・ファースト戦略に熱中してきましたが、私がいつも本当に愛しているのは、便利できちんと文書化されたAPIです。私の目標は、当社のAPIを使用するお客様の体験を、私がお手伝いできる最高のものにすることです。