Мотивация
Я поддерживаю небольшой проект с диковинными инструментами для динозавров. Код проекта полностью открыт и доступен на 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)
Surrogate
26.10.2023 09:35+1в семейство пакетов и программ Microsoft Office
В "семейство" то он входил! Даже в названии продукта было MS Office Visio много лет. Логотип офиса тоже присутствовал!
Я что-то не припоминаю, чтобы в продаже были диски с официальными дистрибутивами Word/Excel/Access/Outlook…
Я спрашивал про это у менеджера по продукту из московского офиса Microsoft. Ценник на Visio в 2-3 раза выше, чем за весь Office вместе взятый…
Surrogate
26.10.2023 09:35У них была альтернатива или Визио в подарок к Офису, или ставить конский ценник на комплект Офис+Визио)
pavelsc
26.10.2023 09:35Мотивация интересная, конечно, но как пользователь отличит, что файл не на сервер грузится, а в браузере обработка идёт? Конечно если у него не диалап
nbelyh Автор
26.10.2023 09:35+1Ну это... Во-первых, ему это просто можно сказать. Или например сделать PWA приложение, которое, работает с отключенным интернетом (все файлы кэшируются локально). В смысле, пользователь может вообще на время конвертации файлов отключить доступ к сети. Кстати для PWA для Astro есть плагин.
enkryptor
26.10.2023 09:35Даже если сразу это непонятно, то при первой же ошибке (проблемы со связью, перегрузка севера или отказ в обслуживании) становится очевидно.
Surrogate
Привет, Николай!
Интересная статья, попробую воспроизвести на досуге :)
Позволь немного позанудствовать: MS Visio ≠ MS Office
Я специально про это в wiki дописал уточнение
nbelyh Автор
Спасибо за отзыв! С точки зрения обработки файлов, формат файла Visio от других форматов Office ничем не отличается, т.е. OpenXml можно применять для обработки любых офисных документов. Здесь интересным является то, что оно нормально работает из WASM.
Surrogate
у тебя в телеге в подписчиках David J Parker! Может он сообщит индусам о твоем достижении)
Может они заинтересуются!!!
Меня из группы рассылки с разработчиками исключили…
nbelyh Автор
Хмм... Ты про каких индусов? Звучит как-то немного настораживающе :D Вообще я сейчас пилю один проект на заказ для индусов, но с данной публикацией он вроде никак не связан (интеграция WOPI)
Surrogate
Понимаю индусов больше миллиарда!
Я имел в виду разработчиков Visio…
itGuevara
Если верить самому 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