Введение



Если вы Web-разработчик и ведете разработку для браузера, то вы точно знакомы с JS, который может исполняться внутри браузера. Существует мнение, что JS не сильно подходит для сложных вычислений и алгоритмов. И хотя в последние годы JS cделал большой рывок в производительности и широте использования, многие программисты продолжают мечтать запустить системный язык внутри браузера. В ближайшее время игра может поменяться благодаря WebAssembly.


Microsoft не стоит на месте и активно пытается портировать .NET в WebAssembly. Как один из результатов мы получили новый фреймворк для клиенской разработки — Blazor. Пока не совсем очевидно, сможет ли Blazor за счет WebAssembly быть быстрее современных JS — фреймворков типа React, Angular, Vue. Но он точно имеет большое преимущество — разработка на C#, а так же весь мир .NET Core может быть использован внутри приложения.


Компиляция и выполение C# в Blazor


Процесс компиляции и выполнения такого сложного языка как C# — это сложная и трудоемкая задача. А можно ли внутри браузера скомпилировать и выполнить С#? — Это зависит от возможностей технологии (а точнее, ядра). Однако в Microsoft, как оказалось, уже все подготовили для нас.


Для начала создадим Blazor приложение.



После этого нужно установить Nuget — пакет для анализа и компиляции C#.


Install-Package Microsoft.CodeAnalysis.CSharp

Подготовим стартовую страницу.


@page "/"
@inject CompileService service

<h1>Compile and Run C# in Browser</h1>

<div>
    <div class="form-group">
        <label for="exampleFormControlTextarea1">C# Code</label>
        <textarea class="form-control" id="exampleFormControlTextarea1" rows="10" bind="@CsCode"></textarea>
    </div>
    <button type="button" class="btn btn-primary" onclick="@Run">Run</button>
    <div class="card">
        <div class="card-body">
            <pre>@ResultText</pre>
        </div>
    </div>
    <div class="card">
        <div class="card-body">
            <pre>@CompileText</pre>
        </div>
    </div>
</div>

@functions
{
    string CsCode { get; set; }
    string ResultText { get; set; }
    string CompileText { get; set; }

    public async Task Run()
    {
        ResultText = await service.CompileAndRun(CsCode);
        CompileText = string.Join("\r\n", service.CompileLog);
        this.StateHasChanged();
    }
}

Для начала надо распарсить строку в абстрактное синтаксическое дерево. Так как в следующем этапе мы будем компилировать Blazor компоненты — нам нужна самая последняя (LanguageVersion.Latest) версия языка. Для этого в Roslyn для C# есть метод:


SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(code, new CSharpParseOptions(LanguageVersion.Latest));

Уже на этом этапе можно обнаружить грубые ошибки компиляции, вычитав диагностику парсера.


            foreach (var diagnostic in syntaxTree.GetDiagnostics())
            {
                CompileLog.Add(diagnostic.ToString());
            }

Далее выполняем компиляцию Assembly в бинарный поток.


CSharpCompilation compilation = CSharpCompilation.Create("CompileBlazorInBlazor.Demo", new[] {syntaxTree},
    references, new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));

using (MemoryStream stream = new MemoryStream())
{
    EmitResult result = compilation.Emit(stream);
}

Следует учесть, что нужно получить references — список метаданных подключенных библиотек. Но прочитать эти файлы по пути Assembly.Location не получилось, так как в браузере файловой системы нет. Возможно, есть более эффективный способ решения этой проблемы, но цель данной статьи — концептуальная возможность, поэтому скачаем эти библиотки снова по Http и сделаем это только при первом запуске компиляции.


foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
{
    references.Add(
        MetadataReference.CreateFromStream(
            await this._http.GetStreamAsync("/_framework/_bin/" + assembly.Location)));
}

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


Assembly assemby = AppDomain.CurrentDomain.Load(stream.ToArray());
var type = assemby.GetExportedTypes().FirstOrDefault();
var methodInfo = type.GetMethod("Run");
var instance = Activator.CreateInstance(type);
return (string) methodInfo.Invoke(instance, new object[] {"my UserName", 12});


На данном этапе мы скомпилировали и выполнили C# код прямо в браузере. Программа может состоять из нескольких файлов и использовать другие .NET библиотеки. Разве это не здорово? Теперь идем дальше.


Компиляция и запуск Blazor компонента в браузере.


Компоненты Blazor — это модифицированные Razor шаблоны. Поэтому чтобы скомпилировать Blazor комопнент, нужно развернуть целую среду для компиляции Razor шаблонов и настроить расширения для Blazor. Нужно установить пакет Microsoft.AspNetCore.Blazor.Build из nuget. Однако, добавить его в наш проект Blazor не получится, так как потом линкер не сможет скомпилировать проект. Поэтому нужно его скачать, а потом вручную добавить 3 библиотеки.


microsoft.aspnetcore.blazor.build\0.7.0\tools\Microsoft.AspNetCore.Blazor.Razor.Extensions.dll
microsoft.aspnetcore.blazor.build\0.7.0\tools\Microsoft.AspNetCore.Razor.Language.dll
microsoft.aspnetcore.blazor.build\0.7.0\tools\Microsoft.CodeAnalysis.Razor.dll

Создадим ядро для компиляции Razor и модифицируем его для Blazor, так как по умолчанию ядро будет генерировать код Razor страниц.


var engine = RazorProjectEngine.Create(BlazorExtensionInitializer.DefaultConfiguration, fileSystem, b =>
    {
        BlazorExtensionInitializer.Register(b);                    
    });

Для выполнения не хватает только fileSystem — это абстракция над файловой системой. Мы реализовали пустую файловую систему, однако, если вы хотите компилировать сложные проекты с поддержкой _ViewImports.cshtml — то нужно реализовать более сложную структуру в памяти.
Теперь сгенерируем код из Blazor компонента C# код.


            var file = new MemoryRazorProjectItem(code);
            var doc = engine.Process(file).GetCSharpDocument();
            var csCode = doc.GeneratedCode;

Из doc можно также получить диагностические сообщения о результатах генерации C# код из Blazor компонента.
Теперь мы получили код C# компонента. Нужно распарсить SyntaxTree, потом скомпилировать Assembly, загрузить её в текущий AppDomain и найти тип компонента. Так же как в предыдущем примере.


Осталось загрузить этот компонент в текущее приложение. Есть несколько способов, как это сделать, например, создав свой RenderFragment.


@inject CompileService service

    <div class="card">
        <div class="card-body">
            @Result
        </div>
    </div>

@functions
{
    RenderFragment Result = null;
    string Code { get; set; }    

    public async Task Run()
    {
            var type = await service.CompileBlazor(Code);
            if (type != null)
            {         
                Result = builder =>
                {
                    builder.OpenComponent(0, type);
                    builder.CloseComponent();
                };
            }
            else
            {             
                Result = null;
            }
    }
}


Заключение


Мы скомпилировали и запустили в браузере Blazor компонент. Очевидно, что полноценная компиляция динамического кода C# прямо внутри браузера может впечатлить любого программиста.


Но тут следует учитывать такие "подводные камни":


  • Для поддержки двунаправленного биндинга bind нужны дополнительные расширения и библиотеки.
  • Для поддержки async, await, аналогично подключаем доп. библиотеки
  • Для компиляции связанных Blazor компонентов потребуется двухэтапная компиляция.

Все эти проблемы уже решены и это тема для отдельной статьи.


GIT


Demo

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


  1. HerrDirektor
    20.12.2018 19:38

    Уболтали, таки попробую еще до НГ эту штуку.


    1. kuber
      21.12.2018 18:52

      Лично я могу рекомендовать каждому c# разработчику попробовать Blazor. Мы уже некоторое время с ним работаем и ещё ни одной строки на JS не написали. А приложение уже кое-что умеет.


      1. Ascar
        22.12.2018 15:32

        Без js целиком не получится, надо хотя бы будет имплементить через JSRuntime.Current.InvokeAsync базовые функции типа alert, да и от кучи полезных библиотек не отказаться никак.


        1. kuber
          23.12.2018 09:29

          Пока что получается. А так я с вами согласен на данный момент библиотек не много, но и jQuery не сразу всеми плагинами оброс. Так что это дело времени.


  1. Ascar
    20.12.2018 22:07

    Одним словом свой рантайм в webassembly и все с этим сопутствующие вещи…


  1. engineX
    20.12.2018 23:17
    +1

    Новый дивный мир, в котором можно выбрать язык программирования для frontend все ближе! :-)


  1. Ne4to
    21.12.2018 02:20

    Занимался последнюю неделю написанием небольшой внутренней админки на Blazor. Есть сложности с отладкой, но целом впечатления сугубо положительные. Вот тут рассказывается об альтернативных моделях хостинга приложения, например Blazor + Electron. Очень интересно во что это в итоге вырастет.


    1. SamProf Автор
      21.12.2018 10:53

      Я заметил, что отлаживать и разрабатывать проще в server-side режиме. Переключать их просто, а отладка и запуск приложения пока быстрее именно в полноценном .NET Runtime.


  1. codecity
    21.12.2018 03:25

    сможет ли Blazor за счет WebAssembly быть быстрее современных JS — фреймворков типа React, Angular, Vue

    Да ладно, быстрее. Будет шикарно, если просто не будет жутких тормозов при открытии страницы. Так как сейчас работает — смысла нет, я уже забыл что открывал. Ваша демка загружалась секунд 30 — это не приемлемо для использования — просто баловство.

    Если кардинально ничего не поменяется — то просто поделка для галочки, не пригодная для реального использования.


    1. ARad
      21.12.2018 08:01

      Несмотря на большую задержку загрузки в СВОИХ областях вполне будет востребовано. К тому же при достаточной популярности браузеры решат проблему начальной загрузки популярных WebAssembly библиотек. Не так сложно это сделать.


      1. codecity
        21.12.2018 12:08

        Несмотря на большую задержку загрузки в СВОИХ областях вполне будет востребовано.

        В таком виде — только для локальных сетей разве что. Если есть вариант доступа через. моб. сети, где скорость плавает — то использовать крайне не комфортно.

        Будем надеяться что это просто недоработка.


        1. engineX
          21.12.2018 20:25

          На Google I/O 2018 был доклад о том, что AutoCAD сделал веб версию на WebAssembly. Они просто смогли скомпилировать и переиспользовать ядро своей системы, написанной на C/C++. Не знаю сколько в итоге длится первый запуск и как много он весит, но мне кажется это пример приложения, загрузку которого можно и подождать ради возможности работать из браузера.


    1. Ascar
      21.12.2018 08:43

      Так автор упомянул как происходит загрузка сборок, судя по путям это с его же сервера тянется. Оптимизация этой загрузки в будущем очевидна.


      1. codecity
        21.12.2018 12:11

        Оптимизация этой загрузки в будущем очевидна.

        Каким образом? Устанавливать сборки вместе с браузером? Или же хранение сборок в локальном кеше браузера?

        Не первое вряд ли пойдут производители браузеров. На второе — нет гарантии что пользователь уже посещал сайты на .Net Core — такие будут ждать 30 сек. и не дождуться.

        Желательная скорость загружки — до 0.8 сек для среднего интернет-канала. Терпимая — до 2-3 сек.


        1. Ascar
          22.12.2018 15:45

          Как я и сказал загрузка была с того же сервера, библиотеки можно залить в облачное хранилище. Еще формат библиотек, их можно сжать и объединить в одну.


          1. codecity
            22.12.2018 15:57

            Боюсь, пока бесперспективно.

            Если сделают компиляцию в нейтивный код, чтобы страница загружалась хотя бы 1 секунду — тогда можно будет вести речь о конкуренции Angular/React. Желательно быстрее. Пока же это, быть может, подойдет для внутренних/локальных сайтов, которыми пользуются работники одного предприятия в добровольно-принудительном порядке (им за это деньги платят).


            1. Ascar
              22.12.2018 16:04

              Еще рано говорить о перспективности, подождем первую версию.


    1. Solexid
      21.12.2018 09:52

      Первая загрузка — 8 секунд. Последуйщие — мгновенно. Так что тут просто загрузка с сервера.


      1. codecity
        21.12.2018 12:14

        Первая загрузка — 8 секунд.

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

        Но и 8 сек. — это жуткий дискомфорт. Не требуется сверх-скорость, но ждать более 2-3 сек. — не приемлемо.


        1. engineX
          21.12.2018 20:08
          +1

          Хочу отметить, что приведенный в статье пример — это не пример использования Blazor как такогового, а пример того, как выполнить компиляцию Blazor «проекта» на стороне клиента.
          Microsoft.CodeAnalysis.* (считай целый компилятор) самые тяжелые библиотеки в этом примере.


          1. codecity
            21.12.2018 21:27

            А не попадался ли вам какой-нибудь сайт Blazor без этих Microsoft.CodeAnalysis.*? Реально ли его использовать или пока это просто эксперимент?


            1. engineX
              21.12.2018 22:05

              Blazor, как фреймворк для разработки приложений, выполняемых на стороне браузера — это определенно эксперимент.

              Это признают даже в Microsoft. Именно по этой причине в новую версию .NET Core 3.0 войдет не Blazor, а Razor Components (server-side Blazor).
              Причина все таже — даже при условии, что вам не нужны тяжеловесы аля Microsoft.CodeAnalysis.* для обычных приложений и использования умного mono linker, который вырезает весь неиспользуемый код, размер минимального приложения довольно велик.

              Один из первых «осмысленных» демо примеров был FlightFinder Может появилось что-то более интересное.


    1. SamProf Автор
      21.12.2018 10:57

      В статье я указал что я сам не на 100% уверен в этом, однако предпосылки в этом есть. Microsoft везде пишет, что это до сих пор экспериментальный фреймворк. Т.е. много проблем еще не решено. На текущий момент они используют Mono-Linker для уменьшения размера dll-к. Но все равно они остаются полноценными CLR — Dll-ками. Обещают, что позже будет компиляция в нативный WASM, соответственно и скорость может быть значительно ваше.
      А пока — можно только гадать, что будет дальше.


  1. stopwaiting
    21.12.2018 14:40

    Ура! +1 способ исправить

    CORS header 'Access-Control-Allow-Origin' does not match…

    А вообще-то немного не по себе от того что мой браузер тихо молча скачал dll и запустил код который потенциально может содержать что угодно включая свежайшие эксплоиты.

    Прощайте сообщения «нужно обновить флеш плеер». Mono cможет сама себя обновить.


    1. SamProf Автор
      21.12.2018 14:43

      Вы немного не верно понимаете технологию. Файтически они поднимают внутри браузера полноценную .NET CLR Runtime. И интерпретатор DLL. Но они исполняются внутри виртуальной машины WASM. Т.е. никакого вмешательства в дела ОС быть не может.
      Но баги… конечно могут быть всегда.

      А Cors — это не проблема, а защита. И она останеться, потому что все запросы из WASM полностью подчиняются правилам браузера.


    1. Whuthering
      21.12.2018 16:19
      +1

      … скачал dll и запустил его в виртуальной машине, которая, в свою очередь, работала в другой виртуальной машине, которая, в свою очередь, сидит в sandbox'e процесса браузера.
      Что, согласитесь, совсем не то, что и загрузка бинаря адресном пространстве браузера со всеми его правами.


      1. staticlab
        22.12.2018 15:57

        А потом переписать Electron-приложения на Blazor :)


    1. Rast1234
      21.12.2018 18:26

      Он не выполнит произвольную DLL. Только ту, в которой есть .net assembly и которая собственно сможет выполниться в .net машине, которая запущена в браузерной песочнице.


    1. Ascar
      22.12.2018 15:49

      CORS разрешается вроде как на стороне сервера…


  1. devlev
    21.12.2018 17:05
    +1

    Автору спасибо за статью. Blazor реально крут. Но — но медленный. Холодный старт 15мб и 12сек времени это на гарантированной скорости в 100мб/с и среднем железе. Тоже пробовал написать свой Hello World и был немного разочарован скоростью. Как верстальщику мне жутко еще не понравилось real-time обновление страниц при небольших изменениях — порядка 8-10 секунд (тестил в студии, готовый пример, возможно это можно как-то оптимизировать). Ну и даже Hello World уже прилично весит. В целом, как мне кажется, технология действительно имеет место быть, но пока ее можно использовать в каких то узкоспециализированных проектах, в которых скорость загрузки не важна, а нужно выжать максимум из возможностей браузеров. Может какие то игры делать на этой платформе оправдано. SPA с быстрым холодным стартом точно мимо


    1. SamProf Автор
      21.12.2018 18:03

      На одной из конф слышал, что они планируют уменьшить размер базового кода для продкашена до 1мб.
      Что уже будет очень неплохо как мне кажется. Сейчас конечно да, печально.


  1. Sioln
    21.12.2018 18:22
    +1

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


    Главное, чтобы после того, как MS в очередной раз закопает стюардессу, разработчики не остались без работы и с нерелевантным опытом.


    1. kuber
      21.12.2018 18:45

      Нет, это не Silverlight. Тут весь смысл в том, что WebAssembly разрабатывает не Microsoft, а сообщество.


      1. staticlab
        22.12.2018 15:58

        WebAssembly напрямую не связан с Blazor API.


        1. kuber
          23.12.2018 09:33

          Именно так. Он связан через Mono т.е. в принципе любой желающий может взять и разработать свой Blazor не приложив для этого титанический усилий.


          1. staticlab
            23.12.2018 11:27

            Усилия будут. В случае с флешем и сильверлайтом рантайм устанавливается на компьютер один раз, а здесь нужно тащить его с собой. Плюс, пока из wasm напрямую не доступен Web API придётся организовывать интероп с JS, а это отдельные накладные расходы и довольно много кода. Чтобы всё это минимизировать, нужно потрудиться.


            1. kuber
              23.12.2018 11:50

              Конечно будут, но не титанические. jQuery же тащат с собой и не жалуются и не просят его встроить в браузер. Продукт ещё даже не вышел, поэтому не надо смотреть на текущий момент. Главное это идея. Они за год настолько сильно изменили Blazor, что очень интересно посмотреть что с ним будет через год, другой.


              1. staticlab
                23.12.2018 12:07

                Кто jQuery тащит?


                1. kuber
                  23.12.2018 12:32
                  +1

                  Странный вопрос. Сайты, которые его используют.


  1. kuber
    21.12.2018 18:26

    Уже пару месяцев работаем с Blazor. Пытаемся понять подойдёт ли он нам. Ощущения очень хорошие. Проблемы со скоростью первого запуска есть, но в десктоп браузере они не так заметны, да и надо понимать, что он ещё не вышел.
    Также в сторону wasm уже идёт c++ и go.
    Конечно, надо ещё доработать процесс отладки. Немного огорчило, что нельзя напрямую из клиента обратится в WCF сервис.


    1. engineX
      21.12.2018 22:20

      Вы используете Blazor для публично доступного сайта или для внутреннего (корпоративного) приложения?


      1. kuber
        21.12.2018 23:09

        Для внутреннего и только в целях тестирования возможностей.


  1. EvilBeaver
    22.12.2018 03:08

    Аааа!!! Где вы раньше были?! Я как раз сейчас с двухэтапной компиляцией мучаюсь. Компоненты, объявленные в компилируемой же сборке упорно не подхватываются даже при двухэтапной сборке. Ощущение, что addTagHelper не видит саму сборку. Че делать и куда копать не понял пока...


  1. justboris
    23.12.2018 12:45
    +1

    А может кто-то из уважаемой аудитории разъяснить, чем Blazor так хорош и почему его не ждет судьба GWT?

    Ответ «потому что WebAssembly» ситуацию не проясняет, потому что одной этой составляющей для успеха маловато.