Создайте веб-приложение Contact Manager, которое обновляется в режиме реального времени, используя Butterfly Server для C #

Почти каждое веб-приложение должно обрабатывать базовые операции CRUD (создание, чтение, обновление и удаление). В этом посте мы покажем, как выполнять эти базовые операции CRUD, создав простой диспетчер контактов с интерфейсом Vue.js и сервером Butterfly Server, где изменения автоматически синхронизируются со всеми клиентами.

Хотя в этом примере используется Vue.js, клиент-бабочка - это ванильный javascript, который работает с любым фреймворком.

Конечный результат выглядит так ...

Предполагается, что у вас уже установлены Visual Studio и npm.

Просто позволь мне попробовать

Предпочитаете пропустить пошаговые инструкции?

Запустите это в терминале или в командной строке…

git clone https://github.com/firesharkstudios/butterfly-server
cd butterfly-server\Butterfly.Example.Contacts
dotnet run

Запустите это во втором терминале или в командной строке…

cd butterfly-server\Butterfly.Example.Contacts\www
npm install
npm run serve

Вы должны увидеть http: // localhost: 8080 / открытым в браузере. Попробуйте открыть второй экземпляр браузера по адресу http: // localhost: 8080 /. Обратите внимание, что изменения автоматически синхронизируются между двумя экземплярами браузера.

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

Создание сервера

Мы будем использовать Butterfly Server для создания нашего сервера. У сервера будут две ключевые обязанности ...

  • Определите веб-API, который позволяет добавлять, обновлять и удалять контакты.
  • Определите API подписки, который позволяет получать список контактов и позволяет получать любые изменения в нашем списке контактов.

Итак, приступим ...

  • Создайте новый проект .NET Core Console App в Visual Studio
  • В консоли диспетчера пакетов выполните Install-Package Butterfly.Db Butterfly.Web.EmbedIO

Теперь у нас есть проект и установлены необходимые зависимости.

Затем давайте создадим базовый каркас для Program.cs, который запускает веб-сервер EmbedIO, прослушивающий порт 8000…

using System;
using Butterfly.Db.Memory;
using Butterfly.Util;
using Butterfly.Web.EmbedIO;
using Butterfly.Web.WebApi;
using Dict = System.Collections.Generic.Dictionary<string, object>;
namespace MyCrudApp {
  class Program {
    static void Main(string[] args) {
      using (var ctx = new EmbedIOContext("http://+:8000/")) {
        // Create database
        // Define Web API
        // Define Subscription API
        // Start the web server and wait        
        ctx.Start();
        Console.ReadLine();
      }
    }
  }
}

Теперь давайте настроим MemoryDatabase для хранения всех наших контактов.

Примечание. Мы также могли использовать базу данных MySQL, Postgres, SQLite или MS SQL Server с минимальными изменениями в нашем коде.

// Create database
var database = new Butterfly.Core.Database.Memory.MemoryDatabase();
database.CreateFromSqlAsync(@"CREATE TABLE contact (
 id VARCHAR(50) NOT NULL,
 first_name VARCHAR(40) NOT NULL,
 last_name VARCHAR(40) NOT NULL,
 PRIMARY KEY(id)
);").Wait();
database.SetDefaultValue("id", 
  table => $"{table.Abbreviate()}_{Guid.NewGuid().ToString()}"
);

Приведенный выше код будет…

  • Создайте экземпляр нашей MemoryDatabase
  • Создайте таблицу контактов
  • Задайте значение по умолчанию для поля id со значениями типа c_a7329696-a334–4118–88be-e773bb78d2e2

Затем давайте определим наш веб-API

// Define the Web API
ctx.WebApi.OnPost("/api/contact/insert", async (req, res) => {
    var record = await req.ParseAsJsonAsync<Dict>();
    await database.InsertAndCommitAsync<string>("contact", record);
});
ctx.WebApi.OnPost("/api/contact/update", async (req, res) => {
    var record = await req.ParseAsJsonAsync<Dict>();
    await database.UpdateAndCommitAsync("contact", record);
});
ctx.WebApi.OnPost("/api/contact/delete", async (req, res) => {
    var id = await req.ParseAsJsonAsync<string>();
    await database.DeleteAndCommitAsync("contact", id);
});

Приведенный выше код будет обрабатывать запросы API следующим образом…

  • Когда получен POST-запрос к / api / contact / insert, проанализируйте запись из тела как JSON и используйте эту запись для вставки новая запись в таблице контактов
  • Когда получен POST-запрос к / api / contact / update, проанализируйте запись из тела как JSON и используйте эту запись для обновления существующая запись в таблице contact
  • Когда получен POST-запрос к / api / contact / delete, проанализируйте id из тела как JSON и используйте этот id для удаления существующая запись в таблице contact

Наконец, давайте определим наш Subscription API

// Define the Subscription API
ctx.SubscriptionApi.OnSubscribe(
    "all-contacts",
    (vars, channel) => database.CreateAndStartDynamicViewAsync(
        "SELECT * FROM contact",
        dataEventTransaction => channel.Queue(dataEventTransaction)
    )
);

Приведенный выше код…

  • Определяет канал всех контактов, на который клиенты могут подписаться
  • Когда клиенты подписываются на канал all-contacts, обработчик создает DynamicView, который сначала отправляет клиенту все записи contact, а затем продолжает отправлять клиенту любые изменения в записях контактов

Примечание. Под капотом API подписки использует WebSockets для передачи данных клиенту. Ожидается, что клиенты будут поддерживать открытый WebSocket для сервера. Это происходит автоматически, когда клиенты используют butterfly-client.

На стороне сервера все.

Создание клиента

Давайте воспользуемся npm и @v ue / cli для создания нашего клиентского проекта…

# Install @vue/cli (skip if already installed)
npm install -g @vue/cli
# Create a skeleton vuetifyjs/pwa project
# (accept all the defaults)
vue create contacts-client
# Install the necessary npm packages
cd contacts-client
vue add vuetify
vue install vue-router butterfly-client

Затем отредактируйте src / main.js, чтобы он выглядел так…

import Vue from 'vue'
import App from './App.vue'
import router from './router'
import vuetify from './plugins/vuetify';
Vue.config.productionTip = false
import { ArrayDataEventHandler, WebSocketChannelClient } from 'butterfly-client'
new Vue({
  router,
  vuetify,
  render: h => h(App),
  data() {
    return {
      channelClient: null,
      channelClientState: null,
    }
  },
  methods: {
    callApi(url, rawData) {
      return fetch(url, {
        method: 'POST',
        body: JSON.stringify(rawData),
        mode: 'no-cors'
      });
    },
    subscribe(options) {
      let self = this;
      self.channelClient.subscribe({
        channel: options.channel,
        vars: options.vars,
        handler: new ArrayDataEventHandler({
          arrayMapping: options.arrayMapping,
          onInitialEnd: options.onInitialEnd,
          onChannelMessage: options.onChannelMessage
        }),
      });
    },
    unsubscribe(key) {
      let self = this;
      self.channelClient.unsubscribe(key);
    },
  },
  beforeMount() {
    let self = this;
    let url = `ws://localhost:8000/ws`;
    self.channelClient = new WebSocketChannelClient({
      url,
      onStateChange(value) {
        self.channelClientState = value;
      }
    });
    self.channelClient.connect();
  },
}).$mount('#app')

Приведенный выше код…

  • Создает экземпляр WebSocketChannelClient, который поддерживает соединение WebSocket с нашим Сервером бабочек
  • Определяет метод callApi (), который наш клиент может использовать для вызова вызовов API.
  • Определяет методы subscribe () и unsubscribe (), которые наш клиент может использовать для подписки / отказа от подписки на определенные каналы на нашем сервере бабочек.

Затем отредактируйте src / App.vue, чтобы он содержал…

<template>
  <v-app>
    <v-content>
      <v-toolbar>
        <v-toolbar-title>My CRUD Example</v-toolbar-title>
        <v-spacer />
      </v-toolbar>
      <router-view v-if="$root.channelClientState=='Connected'"/>
      <div class="px-5 py-5 text-xs-center" v-else>
        <v-progress-circular indeterminate color="primary"/>
        <span class="pl-2 title">
          {{ $root.channelClientState }}...
        </span>
      </div>
    </v-content>
  </v-app>
</template>

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

Затем отредактируйте src / components / HelloWorld.vue, чтобы он содержал…

<template>
  <v-container fluid>
    <!-- Contact List -->
    <v-list v-if="contacts.length>0">
      <v-list-item v-for="contact in contacts" :key="contact.id">
        <v-list-item-content>
          <v-list-item-title>
            {{ contact.first_name }} {{ contact.last_name }}
          </v-list-item-title>
        </v-list-item-content>
        <v-list-item-action>
          <v-btn icon @click="showDialog(true, contact)">
            <v-icon>mdi-edit</v-icon>
          </v-btn>
        </v-list-item-action>
        <v-list-item-action>
          <v-btn icon @click="remove(contact.id)">
            <v-icon>mdi-delete</v-icon>
          </v-btn>
        </v-list-item-action>
      </v-list-item>
    </v-list>
    <v-flex class="px-5 py-5 text-xs-center" v-else>
      - No Contacts -
    </v-flex>
    <!-- Add Contact Button -->
    <v-flex class="text-xs-center py-3">
      <v-btn @click="showDialog(true)" color="primary">
        <v-icon>mdi-add</v-icon> Add Contact
      </v-btn>
    </v-flex>
    <!-- Contact Dialog -->
    <v-dialog v-model="dialogShow" persistent max-width="500px">
      <v-card>
        <v-card-title>
          <span class="headline" v-if="dialogId">
            Update Contact
          </span>
          <span class="headline" v-else>Add Contact</span>
        </v-card-title>
        <v-card-text>
          <v-container grid-list-md>
            <v-layout wrap>
              <v-flex xs12 sm6>
                <v-text-field label="First Name"
                 v-model="dialogFirstName" autofocus required />
              </v-flex>
              <v-flex xs12 sm6>
                <v-text-field label="Last Name"
                 v-model="dialogLastName" required>
                </v-text-field>
              </v-flex>
            </v-layout>
          </v-container>
        </v-card-text>
        <v-card-actions>
          <v-spacer></v-spacer>
          <v-btn color="blue darken-1" text
                 @click="showDialog(false)">Close</v-btn>
          <v-btn color="blue darken-1" dark
                 @click="save">Save</v-btn>
        </v-card-actions>
      </v-card>
    </v-dialog>
  </v-container>
</template>
<script>
  export default {
    data () {
      return {
        contacts: [],
        dialogShow: false,
        dialogId: null,
        dialogFirstName: null,
        dialogLastName: null,
      }
    },
    methods: {
      showDialog(show, item) {
       this.dialogShow = show;
       this.dialogId = (item || {}).id;
       this.dialogFirstName = (item || {}).first_name;
       this.dialogLastName = (item || {}).last_name;
      },
      save() {
        let data = {
          first_name: this.dialogFirstName,
          last_name: this.dialogLastName,
        };
        let apiUrl;
        if (this.dialogId) {
          apiUrl = 'http://localhost:8000/api/contact/update';
          data.id = this.dialogId;
        }
        else {
          apiUrl = 'http://localhost:8000/api/contact/insert';
        }
        let self = this;
        this.$root.callApi(apiUrl, data)
          .then(() => self.dialogShow = false);
      },
      remove(id) {
        this.$root.callApi('http://localhost:8000/api/contact/delete', id);
      },
    },
    mounted() {
      this.$root.subscribe({
        channel: 'all-contacts',
        arrayMapping: {
          contact: this.contacts,
        },
      });
    },
    destroyed() {
      this.$root.unsubscribe('all-contacts');
    }
  }
</script>

Итак, новый HelloWorld.vue выше…

  • Подписка на канал все контакты при подключении компонента (синхронизация всех записей контактов на сервере с локальным массивом контактов)
  • Отменяется от подписки на канал all-contacts при уничтожении компонента
  • Отображает диалоговое окно Контакт, когда пользователь нажимает кнопку Добавить контакт.
  • Отображает диалоговое окно Контакт, когда пользователь щелкает значок редактирования на контакте
  • Вызывает либо / api / contact / insert, либо / api / contact / update, когда пользователь нажимает кнопку Сохранить в диалоговом окне, передавая соответствующие данные
  • Вызывает / api / contact / delete, когда пользователь нажимает значок удаления на контакте

Пробовать это

В Visual Studio запустите серверное приложение. Это запустит сервер и откроет экземпляр браузера по адресу http: // localhost: 8080 /.

Вы даже можете открыть несколько браузеров по адресу http: // localhost: 8080 / и посмотреть, как Butterfly Server синхронизирует всех подключенных клиентов…

Вот и все. Вы хорошо начали создавать веб-приложение CRUD с помощью Butterfly Server.

Резюме

Butterfly Server позволяет создавать гораздо более сложные веб-приложения, в которых клиенты автоматически синхронизируются с сервером. См. Https://butterflyserver.io для получения дополнительной информации.