Мотивация

Я поддерживаю небольшой проект с диковинными инструментами для динозавров. Код проекта полностью открыт и доступен на GitHub. Он умеет обрабатывать файлы офиса (Microsoft Visio) без установки оного (с помощью библиотек для работы с офисными документами OpenXml, PdfSharp). Обработка файлов ведется на .NET (C#).

Раньше обработка документов производилась на сервере (т.е. файл отправлялся на сервер через интернет). Однако это имеет, как минимум, следующие недостатки:

  • Отправка файлов и по сети занимает время,

  • Хостинг сервера небесплатен, даже если это Azure Function или AWS Lambda,

  • Пользователи боятся отправлять свои драгоценные файлы не пойми куда

Посему решил попробовать использовать новые возможности компиляции кода .NET в WebAssembly, добавленные в .NET 7. Раньше что-то похожее тоже было можно сделать, но там отовсюду торчали уши Blazor, а это значит специальный тип приложения, в общем не так просто добавить в уже существующее. К тому же, назовите меня субъективным, но мне не очень нравятся "хайповые" штуки. Сейчас, в .NET 7 Blazor "отцепили", и оно научилось компилировать в WASM без странных довесков.

По сути все что мне требовалось сделать это заменить вызов API сервера на локальный вызов кода из WASM (из компонента React). Далее процесс по шагам.

Проект для .NET

Подробную инструкцию можно найти на сайте Microsoft или вот например в этом блоге. Здесь небольшая выжимка. Предполагается, что вы используете командную строку (я сам использую Visual Studio Code в качестве редактора). Итак,

Устанавливаем поддержку компиляции в WASM. Устанавливается как отдельный workload. Работает начиная с .NET 7

dotnet workload install wasm-tools

Создаем новый проект

dotnet new console

Подправляем его, чтобы он компилировал в WASM. Альтернативно, можно установить `wasm-experimetal`, который добавит шаблон проекта. Но по сути, для изменения проекта консольного приложения на компиляцию в WASM достаточно добавить в проект несколько строк. Детали можно прочитать на сайте Microsoft. Кроме изменения проекта, нужно еще создать (можно пустой) файл "main.js" (нужен для нормальной работы тулсета, думаю в бедующем починят и необходимость в нем отпадет). В моем случае, я просто создал пустой файл.

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net7.0</TargetFramework>

    <!-- добавляем вот этот кусок -->
    <RuntimeIdentifier>browser-wasm</RuntimeIdentifier>
    <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
    <WasmMainJSPath>main.js</WasmMainJSPath>
    <!-- конец -->
    
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

</Project>

Далее, пишем собственно код в Program.cs. Для этого примера я просто возвращаю длину переданных данных (условного "фала"), понятно что на самом деле в реализации должна быть какая-то разумная обработка. В моем случае можно посмотреть что там на самом деле в репозитории GitHub (файлы разбираются/собираются с помощью OpenXml и PdfSharp).

using System.Runtime.InteropServices.JavaScript;
using System.Threading.Tasks;

// Еще один "артефакт". Создает Main, который нуже ндля работы тулсета
return;

public partial class FileProcessor
{
    // Экспортируем метод
    [JSExport]
    internal static async Task<int> ProcessFile(byte[] file)
    {
		await Task.Delay(100); // эмулируем работу
        return file.Length;
    }
}

В общем этого достаточно, компилируем (можно сразу publish, размер будет поменьше)

dotnet publish -c Release

На выходе получаем папку с файлами "bin/Release/net7.0/browser-wasm/AppBundle". Все DLL файлы лежат в "managed". Из всего этого добра на фронтенде нас интересует только файл "dotnet.js", который собственно и предназначен для интеграции в приложение на javascript. Но на хостинг заливается вся папка, т.е. все файлы в ней, включая вложенные директории. Размер не такой страшный, для "Hello World" это несколько мегабайт.

файлы после компиляции
файлы после компиляции

Веб-проект (vite/react)

Для VisioWebTools у меня используется Astro, но для статьи, для простоты, предположим что фронт у нас это vite + react. Ниже я просто делаю новый проект "myapp" для иллюстрации.

npm create vite@latest -- myapp --template react-ts
cd myapp
npm install
npm run dev

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

import { useDotNet } from './useDotNet'

function App() {

  const { dotnet, loading } = useDotNet('/path/to/your/AppBundle/dotnet.js')

  const fileSelected = async (e: any) => {

    const file = e.target.files[0];
    const data = new Uint8Array(await file.arrayBuffer());

    const result = await dotnet.FileProcessor.ProcessFile(data)

    alert(`Result: ${result}`);
  }

  return (
    <>
      <input disabled={loading} type="file" onChange={fileSelected}></input>
    </>
  )
}

export default App

Для удобства использования я сделал кастомный хук ("загрузчик"), useDotNet (код ниже). Я не смог заставить бандлер (ни vite, ни webpack) нормально паковать код из AppBundle (включая DLL и другие "интересные" файлы), поэтому используется динамическая загрузка всего этого добра через await import(url). Параметр "url" должен указывать на "dotnet.js", сгенерированный .NET

import { useEffect, useRef, useState } from 'react';

export const useDotNet = (url: string) => {

  const dotnetUrl = useRef('');
  const [dotnet, setDotNet] = useState<any>(null);
  const [loading, setLoading] = useState(true);

  const load = async (currentUrl: string): Promise<any> => {

    const module = await import(/* @vite-ignore */ currentUrl);

    const { getAssemblyExports, getConfig } = await module
      .dotnet
      .withDiagnosticTracing(false)
      .create();

    const config = getConfig();
    const exports = await getAssemblyExports(config.mainAssemblyName);
    return exports;
  }

  useEffect(() => {
    if (dotnetUrl.current !== url) { // safeguard to prevent double-loading
      setLoading(true);
      dotnetUrl.current = url;
      load(url)
        .then(exports => setDotNet(exports))
        .finally(() => setLoading(false))
    }
  }, [url]);
  return { dotnet, loading };
}

Сборка (CI) и хостинг проекта на GitHub

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

Для сборки проекта я использовал GitHub Actions, для хостинга - GitHub Pages. Бесплатно на сборку оно дает 2000 минут в месяц, что для меня более чем достаточно. Логика такая:

  • Собираем проект .NET,

  • Содержимое папки "AppBundle" выкладываем в папку "public" проекта React/Vite. Содержимое этой папки будет просто скопировано в корень собранного приложения (т.е. в корень папки "dist" в данном конкретном случае),

  • Собираем React приложение в режиме "продакшен". При этом в качестве URL, который используется для загрузки dotnet.js подставляется "/AppBundle/dotnet.js", т.е. ссылка на корень приложения.

GitHub Pages обрабатывает такое нормально, т.е. не ругается на странные типы файлов ".dll" или ".blat" (IIS ругается, для него их надо явно добавлять). Пример конкретной конфигурации для GitHub Actions можно посмотреть здесь.

Заключение

Тестовый ("игрушечный") репозиторий здесь. На моей машине все работает :) На безошибочность не претендую, буду рад любым замечаниям и предложениям. На вопросы, абы такие будут, постараюсь отвечать в меру своего понимания. Буду рад если данная статья окажется вам полезна.

Комментарии (12)


  1. Surrogate
    26.10.2023 09:35
    +1

    Привет, Николай!

    Интересная статья, попробую воспроизвести на досуге :)

    Он умеет обрабатывать файлы офиса (Microsoft Visio)

    Позволь немного позанудствовать: MS Visio ≠ MS Office

    Я специально про это в wiki дописал уточнение

    Первоначально Visio разрабатывался и выпускался компанией Shapeware, затем переименованной в Visio Corporation. Microsoft приобрела компанию в 2000 году[3], тогда продукт назывался Visio 2000. После этого к названию продукта был добавлен префикс Microsoft Office (так продолжалось до версии Visio 2007 (12.0)). Несмотря на это, продукт никогда в пакет Microsoft Office не входил и всегда распространяется отдельно.


    1. nbelyh Автор
      26.10.2023 09:35
      +2

      Спасибо за отзыв! С точки зрения обработки файлов, формат файла Visio от других форматов Office ничем не отличается, т.е. OpenXml можно применять для обработки любых офисных документов. Здесь интересным является то, что оно нормально работает из WASM.


      1. Surrogate
        26.10.2023 09:35

        у тебя в телеге в подписчиках David J Parker! Может он сообщит индусам о твоем достижении)

        Может они заинтересуются!!!
        Меня из группы рассылки с разработчиками исключили…


        1. nbelyh Автор
          26.10.2023 09:35

          Хмм... Ты про каких индусов? Звучит как-то немного настораживающе :D Вообще я сейчас пилю один проект на заказ для индусов, но с данной публикацией он вроде никак не связан (интеграция WOPI)


          1. Surrogate
            26.10.2023 09:35

            Ты про каких индусов?

            Понимаю индусов больше миллиарда!

            Я имел в виду разработчиков Visio…


    1. itGuevara
      26.10.2023 09:35

      Несмотря на это, продукт никогда в пакет Microsoft Office не входил

      Если верить самому MS (и web.archive.org), то формально он все же входил в состав Microsoft Office 2000 (в семейство пакетов и программ Microsoft Office):

      https://web.archive.org/web/20010331085428/http://microsoft.com/office/products.htm

      Хотя при раскрытии состава, видно, что его там нет (standart - premium). Видимо, чтобы "всех запутать" и как следствие, публикации того времени, так и считали:

      https://compress.ru/article.aspx?id=12477


  1. Surrogate
    26.10.2023 09:35
    +1

    в семейство пакетов и программ Microsoft Office

    В "семейство" то он входил! Даже в названии продукта было MS Office Visio много лет. Логотип офиса тоже присутствовал!

    Диск с дистрибутивом
    Диск с дистрибутивом

    Я что-то не припоминаю, чтобы в продаже были диски с официальными дистрибутивами Word/Excel/Access/Outlook…

    Я спрашивал про это у менеджера по продукту из московского офиса Microsoft. Ценник на Visio в 2-3 раза выше, чем за весь Office вместе взятый…


  1. Surrogate
    26.10.2023 09:35


  1. Surrogate
    26.10.2023 09:35

    У них была альтернатива или Визио в подарок к Офису, или ставить конский ценник на комплект Офис+Визио)


  1. pavelsc
    26.10.2023 09:35

    Мотивация интересная, конечно, но как пользователь отличит, что файл не на сервер грузится, а в браузере обработка идёт? Конечно если у него не диалап


    1. nbelyh Автор
      26.10.2023 09:35
      +1

      Ну это... Во-первых, ему это просто можно сказать. Или например сделать PWA приложение, которое, работает с отключенным интернетом (все файлы кэшируются локально). В смысле, пользователь может вообще на время конвертации файлов отключить доступ к сети. Кстати для PWA для Astro есть плагин.


    1. enkryptor
      26.10.2023 09:35

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