Создайте приложение для чата, используя GraphQL, VueJS и мощь современных инструментов разработки программного обеспечения.

Мой отец любит напоминать мне, что, будучи компьютерным инженером в 1970-х, «он был кодером до того, как программирование стало крутым». Один или два раза он даже вытащил старые скрипты Fortran и COBOL. Прочитав этот код, я с уверенностью могу сказать, что кодирование сегодня определенно круче!

Замечательная черта современных компьютерных языков и сред разработки - это то, насколько меньше кода они позволяют писать разработчику. Используя языки высокого уровня вместе с множеством доступных API, пакетов с открытым исходным кодом и платных сервисов, приложения - даже со сложными требованиями - можно создавать невероятно быстро.

Полезным сравнением для осмысления этого момента в эволюции разработки программного обеспечения является рассмотрение конструкции. Когда-то строительство любого дома начиналось с вырубки собственных деревьев. Тем не менее, стандартные материалы, инструменты и передовые методы, разработанные для ускорения выполнения проектов, структур представляют собой более сложную задачу и освобождают рабочих от задач более низкого уровня.

Сколько было бы небоскребов, если бы для строительства одного из них пришлось добывать собственную сталь?

Инженеры, которые еще молоды и работают, начинали свою карьеру с рубки деревьев. Тем не менее, беспрецедентные инновации последнего десятилетия привели к тому, что индустрия программного обеспечения стала созревать так же, как строительство.

Проще говоря, у современных разработчиков теперь есть стандартные инструменты, материалы и передовые методы, которые позволяют быстрее завершать проекты, стабильно работать с приложениями и облегчают разработчикам выполнение задач более низкого уровня.

Обзор учебного пособия по чат-комнате

Давайте построим что-нибудь за считанные минуты, что традиционно занимало бы дни или недели; хорошие новости, это не приложение To-Do! Мы собираемся создать приложение Public Chat Room, которое использует WebSockets для обмена сообщениями в реальном времени.

WebSockets изначально поддерживаются всеми современными браузерами. Однако наша цель - показать, какие инструменты мы можем использовать в работе, а не развивать ее. В этом духе мы будем использовать следующие технологии:

  • 8base - управляемый GraphQL API
  • VueJS - интерфейсный JavaScript-фреймворк‍

Стартовый проект и полный файл README можно найти в этом репозитории GitHub. Если вы хотите просмотреть только готовое приложение, загляните в ветку public-chat-room.

Кроме того, в приведенном ниже видео рассматривается руководство и объясняется каждый шаг в более подробном контексте.

Тем не менее, поехали!

1. Настройка проекта

Клонируйте стартовый проект и перейдите в каталог группового чата. Независимо от того, используете ли вы yarn или npm для установки зависимостей проекта, решать вам. В любом случае нам нужно установить все пакеты NPM, объявленные в файле package.json.

# Clone project
git clone https://github.com/8base/Chat-application-using-GraphQL-Subscriptions-and-Vue.git group-chat
# Move into directory
cd group-chat
# Install dependencies
yarn

Чтобы взаимодействовать с GraphQL API, мы должны установить 3 переменные среды. Создайте файл .env.local в корневом каталоге проекта с помощью следующей команды. Приложение Vue автоматически установит переменные среды, которые мы добавляем в этот файл после инициализации.

echo ‘VUE_APP_8BASE_WORKSPACE_ID=<YOUR_8BASE_WORKSPACE_ID>
VUE_APP_8BASE_API_ENDPOINT=https://api.8base.com
VUE_APP_8BASE_WS_ENDPOINT=wss://ws.8base.com’ \
> .env.local

Оба значения VUE_APP_8BASE_API_ENDPOINT и VUE_APP_8BASE_WS_ENDPOINT всегда одинаковы. Значение, которое нам нужно обновить, - это VUE_APP_8BASE_WORKSPACE_ID.

‍Если у вас есть рабочее пространство 8base, которое вы хотите использовать для этого учебника, продолжайте и обновите свой .env.local файл с идентификатором рабочего пространства. В противном случае получите идентификатор рабочего пространства, выполнив шаги 1 и 2 8base Quickstart.

2. Импорт схемы базы данных

Теперь нам нужно подготовить серверную часть. В корне этого репо вы должны найти файл chat-schema.json. Чтобы импортировать его в рабочую область, нам просто нужно установить и аутентифицировать командную строку 8base, а затем импортировать файл схемы.

# Install 8base CLI
yarn global add 8base-cli
# Authenticate CLI
8base login
# Configure project and select workspace
8base configure
# Import the schema to our workspace
8base import -f chat-schema.json -w <YOUR_8BASE_WORKSPACE_ID>

3. Настройка разрешений API

Последняя задача бэкенда - разрешить публичный доступ к GraphQL API.

В консоли 8base перейдите к App Services > Roles > Guest. Обновите разрешения, установленные для Сообщения и Пользователи, чтобы они были отмечены или Все записи (как показано на снимке экрана). ниже).

Роль гостя определяет, что разрешено делать пользователю, отправившему неаутентифицированный запрос к API.

4. Написание запросов GraphQL

На этом этапе мы собираемся определить и выписать все запросы GraphQL, которые нам понадобятся для нашего компонента чата. Это поможет нам понять, какие данные мы будем читать, создавать и слушать (через WebSockets) через API.

В файл src/utils/graphql.js следует поместить следующий код. Прочтите комментарии над каждой экспортированной константой, чтобы понять, что выполняет каждый запрос.

/* gql converts the query strings into graphQL documents */
import gql from "graphql-tag";
/* 1. Fetch all online chat Users and last 10 messages */
export const InitialChatData = gql`
  {
    usersList {
      items {
        id
        email
      }
    }
    messagesList(last: 10) {
      items {
        content
        createdAt
        author {
          id
          email
        }
      }
    }
  }
`;
/* 2. Create new chat users and assign them the Guest role */
export const CreateUser = gql`
  mutation($email: String!) {
    userCreate(data: { 
      email: $email,
      roles: { 
        connect: { name: "Guest" } 
      } 
    }) {
      id
    }
  }
`;
/* 3. Delete a chat user */
export const DeleteUser = gql`
  mutation($id: ID!) {
    userDelete(data: { id: $id, force: true }) {
      success
    }
  }
`;
/* 4. Listen for when chat users are created or deleted */
export const UsersSubscription = gql`
  subscription {
    Users(filter: { mutation_in: [create, delete] }) {
      mutation
      node {
        id
        email
      }
    }
  }
`;
/* 5. Create new chat messages and connect it to it's author */
export const CreateMessage = gql`
  mutation($id: ID!, $content: String!) {
    messageCreate(
      data: { content: $content, author: { connect: { id: $id } } }
    ) {
      id
    }
  }
`;
/* 6. Listen for when chat messages are created. */
export const MessagesSubscription = gql`
  subscription {
    Messages(filter: { mutation_in: create }) {
      node {
        content
        createdAt
        author {
          id
          email
        }
      }
    }
  }
`;

5. Создание Apollo и SubscriptionClient

Когда наши запросы GraphQL написаны, пришло время настроить наши модули API.

Во-первых, давайте разберемся с клиентом API, использующим ApolloClient с его обязательными настройками по умолчанию. Для createHttpLink мы предоставляем нашу полностью сформированную конечную точку рабочего пространства. Этот код принадлежит src/utils/api.js.

import { ApolloClient } from "apollo-boost";
import { createHttpLink } from "apollo-link-http";
import { InMemoryCache } from "apollo-cache-inmemory";
const { VUE_APP_8BASE_API_ENDPOINT, VUE_APP_8BASE_WORKSPACE_ID } = process.env;
export default new ApolloClient({
  link: createHttpLink({
    uri: `${VUE_APP_8BASE_API_ENDPOINT}/${VUE_APP_8BASE_WORKSPACE_ID}`,
  }),
  cache: new InMemoryCache(),
});
/**
 * Note: To learn more about the options available when configuring
 * ApolloClient, please reference their documentation.
 */

Затем давайте займемся клиентом подписки с помощью subscriptions-transport-ws и isomorphic-ws. Сценарий немного длиннее, чем наш предыдущий, поэтому, пожалуйста, найдите время, чтобы прочитать комментарии в коде! ‍

Мы инициализируем SubscriptionClient с помощью нашей конечной точки WebSockets и workspaceId в connectionParams. Затем мы используем этот subscriptionClient в двух методах, определенных для экспорта по умолчанию; subscribe() и close().

subscribe позволяет нам создавать новые подписки с обратными вызовами данных и ошибок. Метод закрытия - это то, что мы можем использовать, чтобы закрыть соединение при выходе из комнаты чата.

import WebSocket from "isomorphic-ws";
import { SubscriptionClient } from "subscriptions-transport-ws";
const { VUE_APP_8BASE_WS_ENDPOINT, VUE_APP_8BASE_WORKSPACE_ID } = process.env;
/**
 * Create the subscription client using the relevant environment
 * variables and default options
 */
const subscriptionClient = new SubscriptionClient(
  VUE_APP_8BASE_WS_ENDPOINT,
  {
    reconnect: true,
    connectionParams: {
      /**
       * WorkspaceID MUST be set or the Websocket Endpoint 
       * wont be able to map the request to the appropriate
       * workspace
       */
      workspaceId: VUE_APP_8BASE_WORKSPACE_ID
    }
  },
  /**
   * Constructor for W3C compliant WebSocket implementation. 
   * Use this when your environment does not have a built-in 
   * native WebSocket (for example, with NodeJS client)
   */
  WebSocket
);
export default {
  /**
   * Accepts the subscription query, any variables and the
   * callback handlers 'data' and 'error'
   */
  subscribe: (query, options) => {
    const { variables, data, error } = options;
    /**
     * Runs the new subscription request.
     */
    const result = subscriptionClient.request({
      query,
      variables
    });
    /**
     * The unsubscribe function can be used to close a specific
     * subscription as opposed to ALL subscriptions be maintained
     * by the subscriptionClient
     */
    const { unsubscribe } = result.subscribe({
      /**
       * Whenever an event is received, the result is passed
       * to the developer specified data callback.
       */
      next(result) {
        if (typeof data === "function") {
          data(result);
        }
      },
      /**
       * Whenever an error is received, the error is passed
       * to the developer specified error callback.
       */
      error(e) {
        if (typeof error === "function") {
          error(e);
        }
      }
    });
    return unsubscribe;
  },
  /**
   * Closes subscriptionClient's connection.
   */
  close: () => {
    subscriptionClient.close();
  }
};
/** 
 * Note: To learn more about the SubscriptionClient and 
 * its options, please reference their documentation 
 */

6. Написание компонента Vue

На данный момент у нас есть все необходимое для создания нашего публичного чата! Осталось только написать один GroupChat.vue компонент.

Загрузите компонент с yarn serve и продолжим.

ВНИМАНИЕ: красота в глазах смотрящего… и поэтому добавлен только минимальный стиль, необходимый для того, чтобы компонент работал.

Скрипт компонента Vue

Сначала нам нужно импортировать наши модули, простой стиль и запросы GraphQL. Все эти артефакты находятся в нашем src/utils каталоге.

Вставьте следующие операторы импорта после открывающего тега \<script\> в GroupChat.vue.‍

/* API modules */
import Api from "./utils/api";
import Wss from "./utils/wss";

/* graphQL operations */
import {
 InitialChatData,
 CreateUser,
 DeleteUser,
 UsersSubscription,
 CreateMessage,
 MessagesSubscription,
} from "./utils/graphql";
‍
/* Styles */
import "../assets/styles.css";

Данные компонента Vue

Мы можем определить, какие свойства данных мы хотим использовать в функции данных нашего компонента. Все, что нам нужно, - это способ хранить пользователей чата, сообщения, имя «текущего» пользователя и все сообщения, которые еще не были отправлены. Эти свойства можно добавить так:

/* imports ... */

export default {
 name: "GroupChat",
‍
 data: () => ({
   messages: [],
   newMessage: "",
   me: { email: "" },
   users: [],
 }),
};

Хуки жизненного цикла Vue

Наши хуки жизненного цикла выполняются в разные моменты «жизни» компонента Vue. Например, когда он установлен или обновлен. В нашем случае нас интересует только то, когда компонент создан и beforeDestroy. В таких случаях мы хотим либо открыть подписки на чат, либо закрыть подписки на чат.

/* imports ... */
export default {
  /* other properties ... */
  /**
   * Lifecycle hook executed when the component is created.
   */
  created() {
    /**
     * Create Subscription that triggers event when user is 
     * created or deleted
     */
    Wss.subscribe(UsersSubscription, {
      data: this.handleUser
    });
    /**
     * Create Subscription that triggers event when 
     * message is created
     */
    Wss.subscribe(MessagesSubscription, {
      data: this.addMessage
    });
    /**
     * Fetch initial chat data (Users and last 10 Messages)
     */
    Api.query({
      query: InitialChatData
    }).then(({ data }) => {
      this.users = data.usersList.items;
      this.messages = data.messagesList.items;
    });
    /**
     * Callback executed on page refresh to close chat
     */
    window.onbeforeunload = this.closeChat;
  },
  /**
   * Lifecycle hook executed before the component is destroyed.
   */
  beforeDestroy() {
    this.closeChat();
  }
};

Методы компонента Vue

Мы должны добавить определенные методы, предназначенные для обработки каждого вызова / ответа API (createMessage, addMessage, closeChat и т. Д.). Все они будут сохранены в объекте методов нашего компонента.

Стоит отметить одну вещь: большинство мутаций не ждут и не обрабатывают ответы. Это потому, что у нас есть подписки, которые отслеживают эти мутации. После успешного запуска данные о событиях обрабатываются именно подпиской.

Большинство из этих методов говорят сами за себя. В любом случае, пожалуйста, прочтите комментарии в следующем коде.

/* imports ... */

export default {
 /* other properties ... */
‍
 methods: {
   /**
    * Create the new user using a submitted email address.
    */
   createUser() {
     Api.mutate({
       mutation: CreateUser,
       variables: {
         email: this.me.email,
       },
     });
   },
   /**
    * Delete a user by their ID.
    */
   deleteUser() {
     Api.mutate({
       mutation: DeleteUser,
       variables: { id: this.me.id },
     });
   },
   /**
    * Our users subscription listing to both the create 
    * and update events, and therefore we need to choose 
    * the appropriate method to handle the response based 
    * on the mutation type.
    *
    * Here, we have an object that looks up the mutation 
    * type by name, returns it and executes the function 
    * while passing the event node.
    */
   handleUser({
     data: {
       Users: { mutation, node },
     },
   }) {
     ({
       create: this.addUser,
       delete: this.removeUser,
     }[mutation](node));
   },
   /**
    * Adds a new user to users array, first checking 
    * whether the user being added is the current user.
    */
   addUser(user) {
     if (this.me.email === user.email) {
       this.me = user;
     }
     this.users.push(user);
   },
   /**
    * Removes user from the users array by ID.
    */
   removeUser(user) {
     this.users = this.users.filter(
       (p) => p.id != user.id
     );
   },
   /* Create a new message */
   createMessage() {
     Api.mutate({
       mutation: CreateMessage,
       variables: {
         id: this.me.id,
         content: this.newMessage,
       },
     }).then(() => (this.newMessage = ""));
   },
   /**
    * Our messages subscription only listens to the 
    * create event. Therefore, all we need to do is 
    * push it into our messages array.
    */
   addMessage({
     data: {
       Messages: { node },
     },
   }) {
     this.messages.push(node);
   },
   /**
    * We'll want to close our subscriptions and 
    * delete the user. This method can be
    * called in our beforeDestroy lifecycle hook and 
    * any other relevantly placed callback.
    */
   closeChat () {
     /* Close subscriptions before exit */
     Wss.close()
     
     /* Delete participant */
     this.deleteUser();
     
     /* Set me to default */
     this.me = { me: { email: '' } }
   }
 },
‍
 /* lifecycle hooks ... */
}

Шаблон компонента Vue

И последнее, но не менее важное: у нас есть компонент \<template\>.

‍Существуют тысячи отличных руководств по созданию красивого пользовательского интерфейса. Это не одно из тех руководств.

Следующий шаблон соответствует минимальным требованиям приложения для группового чата. Вам действительно нужно пойти и сделать это красивым. Тем не менее, давайте быстро пройдемся по ключевой разметке, которую мы здесь реализовали.

Как всегда, читайте комментарии к встроенному коду.

<template>
 <div id="app">
   <!--
     Only if the current user has an ID (has been created) 
     should the chat view be rendered. Otherwise, a sign 
     up for is shown.
    -->
   <div v-if="me.id" class="chat">
     <div class="header">
       <!--
         Since we're using subscriptions that run in real-time, 
         our number of user currently online will 
         dynamically adjust.
        -->
       {{ users.length }} Online Users
       <!--
         A user can leave the chat by executing the
         closeChat function.
        -->
       <button @click="closeChat">Leave Chat</button>
     </div>
     <!--
       For every message that we're storing in the messages array,
       we'll render out in a div. Additionally, if the 
       messages participant id matches the current user 
       id, we'll assign it the me class.
      -->
     <div
       :key="index"
       v-for="(msg, index) in messages"
       :class="['msg', { me: msg.participant.id === me.id }]"
     >
       <p>{{ msg.content }}</p>
       <small>
          <strong>{{ msg.participant.email }}</strong>
          {{ msg.createdAt}}
       </small>
     </div>
     <!--
       Our message input is bound to the newMessage data property.
      -->
     <div class="input">
       <input
         type="text"
         placeholder="Say something..."
         v-model="newMessage"
       />
       <!--
         When the user clicks the send button, we run 
         the createMessage function.
        -->
       <button @click="createMessage">Send</button>
     </div>
   </div>
   <!--
     The sign up flow simply asks the user to enter an email 
     address. Once the input is blurred, the createUser 
     method is executed.
    -->
   <div v-else class="signup">
     <label for="email">Sign up to chat!</label>
     <br />
     <input
       type="text"
       v-model="me.email"
       placeholder="What's your email?"
       @blur="createUser"
       required
     />
   </div>
 </div>
</template>

Вы не поверите, но теперь весь публичный чат построен. Если вы откроете его в локальной сети (yarn serve), вы сможете отправлять и получать сообщения. Однако, чтобы доказать, что это настоящий групповой чат, откройте несколько окон и наблюдайте за ходом разговора!

7. Заключение и тестирование

В этом руководстве мы изучили, как использование современных инструментов разработки позволяет нам создавать реальные приложения за считанные минуты.

Надеюсь, вы также узнали, как инициализировать ApolloClient и SubscriptionClient для эффективного выполнения запросов GraphQL, изменений и подписок в рабочей области 8base, а также немного о VueJS.‍

Независимо от того, работаете ли вы над веб-игрой, мобильной игрой, приложениями для обмена сообщениями и уведомлениями или над другими проектами, требующими данных в реальном времени, подписки - отличный инструмент для использования. Мы здесь почти не коснулись!

Платформа 8base позволяет разработчикам создавать потрясающие облачные приложения с использованием JavaScript и GraphQL. Узнайте больше о платформе 8base.

Первоначально опубликовано в блоге 8base.