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

В этом примере используется…

Просто дай мне попробовать

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

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

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

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

cd butterfly-server-dotnet\Butterfly.Example.Todo\vue
npm install
npm run dev

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

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

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

Во-первых, давайте создадим новый проект Консольное приложение .NET Core в Visual Studio.

Затем откройте Консоль диспетчера пакетов в Visual Studio и установите зависимости…

Install-Package Butterfly.Core
Install-Package Butterfly.EmbedIO

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

using System;
using Butterfly.Core.Channel;
using Butterfly.Core.Database;
using Butterfly.Core.Util;
using Butterfly.Core.WebApi;
using Dict = System.Collections.Generic.Dictionary<string, object>;

Затем отредактируйте метод Main() в Program.cs, чтобы он был…

static void Main(string[] args) {
  // Create the underlying EmbedIOWebServer
  var webServer = new Unosquare.Labs.EmbedIO.WebServer(8000);
  Unosquare.Swan.Terminal.Settings.DisplayLoggingMessageType =
    Unosquare.Swan.LogMessageType.Info;
  // Create a MemoryDatabase (no persistence, limited features)
  var database = 
    new Butterfly.Core.Database.Memory.MemoryDatabase();
  // Setup webApiServer and channelServer using embedIOWebServer
  using (var webApi = 
    new Butterfly.EmbedIO.EmbedIOWebApi(webServer))
  using (var subscriptionApi = 
    new Butterfly.EmbedIO.EmbedIOSubscriptionApi(webServer)) {
        
    Init(database, webApi, subscriptionApi);
    webApi.Compile();
    subscriptionApi.Start();
    webServer.RunAsync();
    Console.ReadLine();
  }
}

Приведенный выше метод Main()

  • Создает веб-сервер EmbedIO для прослушивания запросов HTTP и WebSocket на порту 8000.
  • Создает базу данных в памяти
  • Создает WebApiServer и ChannelServer, которые обертывают веб-сервер EmbedIO.
  • Запускает веб-сервер EmbedIO

Затем добавьте метод Init() в Program.cs, чтобы он был…

static void Init(IDatabase database, 
  IWebApi webApi, ISubscriptionApi subscriptionApi) {            
  // Setup database
  database.CreateFromTextAsync(@"CREATE TABLE todo (
    id VARCHAR(50) NOT NULL,
    name VARCHAR(40) NOT NULL,
    PRIMARY KEY (id)
  );").Wait();
  database.SetDefaultValue("id", 
    table => $"{table.Abbreviate()}_{Guid.NewGuid().ToString()}");
  // Listen for API requests
  webApi.OnPost("/api/todo/insert", async (req, res) => {
    var todo = await req.ParseAsJsonAsync<Dict>();
    await database.InsertAndCommitAsync<string>("todo", todo);
  });
  webApi.OnPost("/api/todo/delete", async (req, res) => {
    var id = await req.ParseAsJsonAsync<string>();
    await database.DeleteAndCommitAsync("todo", id);
  });
  // Listen for subscribe requests...
  // - The handler must return an IDisposable object 
  //   (gets disposed when the channel is unsubscribed)
  // - The handler can push data to the client by 
  //   calling channel.Queue()
  subscriptionApi.OnSubscribe("todos", (vars, channel) => {
    return database.CreateAndStartDynamicViewAsync(
      "todo", 
      dataEventTransaction => channel.Queue(dataEventTransaction)
    );
  });
}

Приведенный выше метод Init()

  • Создает таблицу todo в базе данных со случайным полем идентификатора GUID.
  • Прослушивает запросы POST в /api/todo/insert, которые вставляют новую запись в таблицу todo.
  • Прослушивает запросы POST в /api/todo/delete, которые удаляют существующую запись в таблице todo.
  • Отслеживает запросы на подписку на канале todos, который возвращает все существующие записи todo и любые изменения в записях todo

Это все для серверной части.

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

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

npm install -g vue-cli
# Just accept the defaults...
vue init vuetifyjs/pwa my-todo-client
cd my-todo-client
npm install
npm install butterfly-client reqwest

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

Затем отредактируйте config/index.js, заменив proxyTable: {} на…

proxyTable: {
  '/api': {
    target: 'http://localhost:8000/',
    changeOrigin: true,
  },
  '/ws': {
    target: 'http://localhost:8000/',
    changeOrigin: true,
    ws: true,
  }
}

Вышеупомянутые изменения позволят серверу разработки узла передавать запросы API и WebSocket нашему Butterfly Server .NET.

Затем отредактируйте src/main.js, чтобы добавить пару импортов…

import { ArrayDataEventHandler, WebSocketChannelClient } from 'butterfly-client'
import reqwest from 'reqwest'

Затем отредактируйте src/main.js, заменив существующий вызов new Vue() на…

new Vue({
  el: '#app',
  router,
  components: { App },
  template: '<App/>',
  data() {
    return {
      channelClient: null,
      channelClientState: null,
    }
  },
  methods: {
    callApi(url, rawData) {
      return reqwest({
        url,
        method: 'POST',
        data: JSON.stringify(rawData),
      });
    },
    subscribe(options) {
      let self = this;
      self.channelClient.subscribe({
        channel: options.key,
        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://${window.location.host}/ws`;
    self.channelClient = new WebSocketChannelClient({
      url,
      onStateChange(value) {
        self.channelClientState = value;
      }
    });
    self.channelClient.connect();
  },
})

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

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

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

<template>
  <v-app>
    <v-content>
      <v-toolbar>
        <v-toolbar-title>My Todo 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 .NET.

Затем отредактируйте src/components/HelloWorld.vue, указав…

<template>
  <v-container fluid>
    <Todos/>
  </v-container>
</template>
<script>
  import Todos from '@/components/Todos'
  export default {
    components: {
      Todos,
    }
  }
</script>

Затем создайте новый src/components/Todos.vue, который содержит…

<template>
  <div>
    <div class="px-3 py-3 text-xs-center" v-if="items.length==0">
      No todos yet
    </div>
    <v-list v-else>
      <Todo v-for="item in items" :key="item.id" :item="item" @remove="remove" />
    </v-list>
<div class="px-3 py-3 text-xs-center">
      <v-btn color="primary" @click="add">Add Todo</v-btn>
    </div>
  </div>
</template>
<script>
  import Todo from '@/components/Todo'
  export default {
    components: {
      Todo,
    },
    data () {
      return {
        items: [],
      }
    },
    methods: {
      add() {
        this.$root.callApi('/api/todo/insert', {
          name: 'A new todo item',
        });
      },
      remove(id) {
        this.$root.callApi('/api/todo/delete', id);
      },
    },
    mounted() {
      let self = this;
      self.$root.subscribe({
        arrayMapping: {
          todo: self.items,
        },
        key: 'todos',
      });
    }
  }
</script>

Вышеупомянутый Todos.vue отвечает за…

  • Вызов функции add() для выполнения вызова API к нашему Butterfly Server .NET, который добавляет новый todo
  • Вызов функции remove() для выполнения вызова API к нашему Butterfly Server .NET, который удаляет существующую todo
  • Подписка на канал todos на нашем Butterfly Server .NET и сопоставление записей todo с локальным массивом items

Затем создайте новый src/components/Todo.vue, который содержит…

<template>
  <v-list-tile>
    <v-list-tile-content>
      <v-list-tile-title>{{ item.name }}</v-list-tile-title>
    </v-list-tile-content>
    <v-list-tile-action>
      <v-btn icon @click="$emit('remove', item.id)">
        <v-icon>delete</v-icon>
      </v-btn>
    </v-list-tile-action>
  </v-list-tile>
</template>
<script>
  export default {
    props: {
      item: null
    }
  }
</script>

Вышеупомянутый Todo.vue отвечает за отрисовку одного todo.

Наконец, отключите eslint, закомментировав этот раздел в build/webpack.base.conf.js

/*
{
  test: /\.(js|vue)$/,
  loader: 'eslint-loader',
  enforce: 'pre',
  include: [resolve('src'), resolve('test')],
  options: {
    formatter: require('eslint-friendly-formatter')
  }
},
*/

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

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

Чтобы попробовать приложение Todo List

  • Запустите сервер в Visual Studio
  • В терминале запустите npm run dev, находясь в каталоге my-todo-client.

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

Следующие шаги

Butterfly Server .NET поддерживает создание гораздо более сложных веб-приложений в реальном времени (может подписываться на каналы с несколькими наборами данных, может объединять несколько таблиц в каждом наборе данных и т. д.). См. GitHub для более подробной информации.