javascript

Создаем веб приложение используя VueJS и .NET

  • суббота, 18 марта 2017 г. в 03:13:53
https://habrahabr.ru/post/324176/
  • JavaScript
  • ASP
  • .NET


Данная стать является переводом вот этой статьи. В ней я расскажу вам как удобно использовать Vue.js фреймворк при разработке приложения на ASP.NET MVC
image

Введение


Последние несколько месяцев я присматривал разные JavaScript фреймворки для интеграции их в свои MVC проекты. Как только вы выбираете что-то в духе React, Angular или Ember для работы с .NET вы должны устанавливать модули-адаптеры, переписывать всю логику маршрутизации для всех контроллеров. В конце концов это влияет на все веб-приложение, когда ты хочешь работать бок о бок с уже готовым рабочим стеком. Если вы начинаете новый проект, целесообразно использовать Web API для бэкенда, который обеспечит REST интерфейс для JS фреймворка на ваш выбор, но для существующих проектов на MVC это не выход.

После небольших исследований я наткнулся на Vue.JS и после некоторых экспериментов мне удалось заставить его работать в связке с MVC. Vue.JS – относительно легковесный фреймворк, так что я могу добавить его в представление, где мне нужны дополнительные возможности JS, тем самым оставляя остальную часть веб-приложения нетронутой. Теперь, когда я использую Vue в продакшене я рад поделится с вами некоторыми примерами, для всех кто разделяет мои проблемы. Мой рабочий процесс заимствует концепции Migel’s Castro [тык] где он использовал ее для интеграции AngularJS с MVC.

Приступаем к работе


  • Для начала нам нужен MVC проект, создайте стандартный MVC 5 проект.
  • Мы дадим npm обработать все наши пакеты, так что создайте в проекте файл package.json.
  • Добавьте в зависимости VueJS

  • {
      "version": "1.0.0",
      "Name": "ASP.NET",
      "private": true,
      "devDependencies": {
      },
      "dependencies": {
            "vue": "^1.0.26"
      }
    }
    

  • Сохраните файл и Visual Studio сама вызовет команду npm install после чего скачает все необходимые пакеты для вас.

Теперь давайте использовать Vue в нашем представлений

  • В представлении по умолчанию (index.cshtml) я удалил стандартную разметку и подключил VueJS из папки node-modules.

  • @{
        ViewBag.Title = "Home Page";
    }
    @Scripts.Render("~/node_modules/vue/dist/vue.min.js")
    

  • Давайте добавим пример Hello World, с документации VueJS, в наше приложение.

  • @{
        ViewBag.Title = "Home Page";
    }
    <div id="app">
        
    </div>
    @Scripts.Render("~/node_modules/vue/dist/vue.min.js")
    
    <script>
        const v = new Vue({
            el: '#app',
            data: {
                message: 'Hello Vue.js!'
            }
        })
    </script>
    

  • Давайте запустим наш проект

  • image

Очевидно, что поместив все ваши скрипты в наше представления — это гарантированный путь к головные боли, поэтому сейчас давайте организуем наш код.

Приступаем к работе


Когда у нас есть большое приложение построенное на VueJS, я разделяю проект на части. Для каждого контроллера у меня есть соответствующее приложение Vue, которое состоит из нескольких Vue компонентов. Отношение один-к-одному между контроллером и приложением делает наш код более читаемым и удобным в поддержке. Каждый модуль приложения будет содержать только JavaScript библиотеки, которые нужны, а не один большой пакет.

JavaScript упаковщики улучшились за эти годы. Browserify позволяет вам использовать стиль node.js модулей для работы в браузере. Мы определяем зависимости и потом Browserify собирает их в один маленький и чистенький JavaScript файл. Вы подключаете ваши JavaScript файлы используя require("./ваш_файл.js"); выражение. Это позволяет нам использовать только те библиотеки, которые нужны. Так что теперь, если мы предположим, каждый контроллер представляет собой некоторый контейнер, каждый из которых содержит один или больше js файлов. Эти файлы затем будут помещены в пакет, который будет размещен в папке нашего проекта, который затем загружает и использует браузер.

Я обычно следую следующую структуру. Весь мой Vue код помещен в папку ViewModel. Для каждого контроллера, который использует Vue я создаю под папку в папке ViewModel, и затем я вызываю контейнеры в файле main.js. После этого я использую Gulp и Browserify для упаковки всех файлов в пакет, который хранится в папке проекта. Представления ссылаются на наши пакеты и когда браузер запрашивает страницу пакет скачивается и запускается.

Немного практики


  • В нашем проекте я создал новую папку ViewModels, а в ней папку Home которая отвечает за контроллер HomeController.
  • В папке ViewModel я также создал файл main.js.

  • image

  • Теперь добавим немного зависимостей

  • "devDependencies": {
        "browserify": "^13.0.0",
        "watchify": "^3.7.0",
        "gulp": "^3.9.1",
        "gulp-util": "^3.0.7",
        "gulp-babel": "^6.1.2",
        "gulp-uglify": "^2.0.0",
        "gulp-sourcemaps": "^1.6.0",
        "fs-path": "^0.0.22",
        "vinyl-source-stream": "^1.1.0",
        "vinyl-buffer": "^1.0.0",
        "babel-preset-es2015": "^6.13.2"
      }
    

  • Добавьте в проект файл gulp и вставте этот код:

  • const gulp = require('gulp');
    const gutil = require('gulp-util');
    var babel = require('gulp-babel');
    var minify = require('gulp-uglify');
    var sourcemaps = require('gulp-sourcemaps');
    const fs = require('fs');
    const path = require('path');
    const browserify = require('browserify');
    const watchify = require('watchify');
    const fsPath = require('fs-path');
    
    var source = require('vinyl-source-stream');
    var buffer = require('vinyl-buffer');
    var es2015 = require('babel-preset-es2015');
    
    function getFolders(dir) {
        return fs.readdirSync(dir)
        .filter(function (file) {
            return fs.statSync(path.join(dir, file)).isDirectory();
        });
    }
    
    const paths = [
        process.env.INIT_CWD + '\\ViewModels\\home',
        process.env.INIT_CWD + '\\ViewModels\\home\\components',
        process.env.INIT_CWD + '\\ViewModels\\common\\components'
    ];
    
    function watchFolder(input, output) {
        var b = browserify({
            entries: [input],
            cache: {},
            packageCache: {},
            plugin: [watchify],
            basedir: process.env.INIT_CWD,
            paths: paths
        });
    
        function bundle() {
            b.bundle()          
                .pipe(source('bundle.js'))
                .pipe(buffer())
                .pipe(sourcemaps.init({ loadMaps: true }))            
                //.pipe(babel({ compact: false, presets: ['es2015'] }))
                // Add transformation tasks to the pipeline here.
                //.pipe(minify())
                //  .on('error', gutil.log)
                .pipe(sourcemaps.write('./'))
                .pipe(gulp.dest(output));
    
            gutil.log("Bundle rebuilt!");
        }
        b.on('update', bundle);
        bundle();
    }
    
    function compileJS(input, output) {
        // set up the browserify instance on a task basis
        var b = browserify({
            debug: true,
            entries: [input],
            basedir: process.env.INIT_CWD,
            paths: paths
        });
    
        return b.bundle()
        .pipe(source('bundle.js'))
        .pipe(buffer())
        .pipe(sourcemaps.init({ loadMaps: true }))
            .pipe(babel({ compact: false, presets: ['es2015'] }))
            // Add transformation tasks to the pipeline here.
            .pipe(minify())
            .on('error', gutil.log)
        //.pipe(sourcemaps.write('./'))
        .pipe(gulp.dest(output));
    }
    
    const scriptsPath = 'ViewModels';
    
    gulp.task('build', function () {
        var folders = getFolders(scriptsPath);
        gutil.log(folders);
        folders.map(function (folder) {
            compileJS(scriptsPath + "//" + folder + "//main.js", "Scripts//app//" + folder);
        });
    });
    
    gulp.task('default', function () {
        var folders = getFolders(scriptsPath);
        gutil.log(folders);
        folders.map(function (folder) {
            watchFolder(scriptsPath + "//" + folder + "//main.js", "Scripts//app//" + folder);
        });
    
    });
    

  • Также здесь мы используем watchify для мониторинга изменений в папке ViewModels, поэтому все файлы, которые были изменены будут пересобраны. Все что нужно сделать это обновить страницу в браузере.

Теперь вернемся к коду

  • Перенесем наше приложение Hello World в main.js.
  • С помощью browserify мы можем установить зависимости
  • Файл main.js должен выглядеть следующим образом:

  • const Vue = require('vue');
    const v = new Vue({
        el: '#app',
        data: {
            message: 'Hello Vue.js!'
        }
    });
    

  • В файле index.cshtml измените ссылки на наш упакованный пакет

  • @{
        ViewBag.Title = "Home Page";
    }
    <div id="app">
        { { message } }
    </div>
    @Scripts.Render("~/Scripts/app/home/bundle.js")
    

  • Обновите страницу
  • Так как при переходе на Vue нам придется устанавливать ссылки для каждого контроллера мы можем создать некий универсальный шаблон.

  • @{
        var controllerName = HttpContext.Current.Request.RequestContext.RouteData.Values["Controller"].ToString();     
    }
    @Scripts.Render("~/Scripts/app/" + controllerName + "/bundle.js")
    

Загрузка данных из сервера


С помощью Vue мы можем извлекать и отображать данные которые приходят с сервера.

  • Добавим некоторые данные в наш контроллер

  • public JsonResult GetData()
    {
        return Json(new
        {
            Name = "Marco",
            Surname = "Muscat",
            Description = "Vue data loaded from razor!"
        },JsonRequestBehavior.AllowGet);
    }
    

  • Добавьте JQuery в package.json и обновите ваше Vue приложение для вызова данных из сервера

  • const Vue = require("vue");
    const $ = require("jquery");
    
    const v = new Vue({
        el: '#app',
        ready: function () {
            this.loadData();
        },
        data: {
            message: 'Hello Vue.js!',
            serverData: null
        },
        methods: {
            loadData: function (viewerUserId, posterUserId) {
                const that = this;
    
                $.ajax({
                    contentType: "application/json",
                    dataType: "json",
                    url: window.ServerUrl + "/Home/GetData",
                    method: "GET",
                    success: function (response) {
                        console.log(response);
                      that.$data.serverData = response;
                    },
                    error: function () {
                        console.log("Oops");
                    }
                });
            }
        }
    })
    

  • Для подключения к серверу, я обычно добавляю глобальную переменную window.ServerUrl для простого доступа к текущему хосту. Если вы хотите сделать что-то подобное, просто добавьте следующий код в файл представления.

  • @{
        var controllerName = HttpContext.Current.Request.RequestContext.RouteData.Values["Controller"].ToString();
        var serverUrl = string.Format("{0}://{1}{2}", Request.Url.Scheme, Request.Url.Authority, Url.Content("~"));
        var controllerUrl = Url.Content("~") + controllerName;
    }
    <script>
    window.ServerUrl = '@serverUrl';
    window.VueRouterUrl = '@controllerUrl';
    </script>
    @Scripts.Render("~/Scripts/app/" + controllerName + "/bundle.js")
    

  • Пересоберите ваш проект и запустите его.
  • Следующим шагом будет отображение полученных данных

  • <div id="app">
        { { message } }
        <br/>
        <span>coming straight from mvc! { {serverData.Name} } { {serverData.Surname} }. { {serverData.Description} }</span>    
    </div>
    

На данный момент этого достаточно для того чтобы использовать vue.js и MVC, но, как и любые другие фронтэнд фреймворки, vue страдает от задержек при загрузке. При запросе страницы, JavaScript должен быть загружен и запущен. Затем фреймворк делает еще несколько запросов к серверу для получения данных. Для предотвращения этого мы должны прибегнуть к анимации загрузки и к другим хакам, но так как мы также используем MVC мы можем сделать лучше и ускорить процесс загрузки, за счет использования Razor и загрузки данных для представления вместе с остальной частью страницы.

Начальная загрузка данных


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

Предупреждение! Этот метод подойдет в том случае если в начальной загрузке участвует небольшое количество данных. В других случаях лучше использовать пагинацию или AJAX запросы для достижения более плавной загрузки страницы и уменьшить задержки.

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

public ActionResult Index()
{
    var serverModel = JsonConvert.SerializeObject(new
    {
        Name = "Marco",
        Surname = "Muscat",
        Description = "Vue data loaded from razor!"
    });

    return View(new SampleModel()
    {
        Data = serverModel
    });
}

public class SampleModel
{
    public string Name { get; set; }
    public string Surname { get; set; }
    public string Description { get; set; }
    public string Data { get; set; }
}

  • SampleModel это простой класс, нужен он нам для того чтобы облегчить связывание в Razor.
  • В Index.cshtml, загрузим наши данные и сериализуем их в JavaScript объект.

  • <script> window.preLoadeddata = JSON.parse('@Html.Raw(Model.Data)')</script>

  • Теперь давайте расскажем Vue откуда брать данные.

  • const Vue = require("vue");
    const $ = require("jquery");
    
    const v = new Vue({
        el: '#app',
        ready: function () {
           
        },
        data: {
            message: 'Hello Vue.js!',
            serverData: window.preLoadeddata
        },
        methods: {
        }
    })
    

Маршрутизация


Наш проект не может считаться законченным пока в нем отсутствует маршрутизация. Более сложные приложения потребуется несколько представлений, так как там слишком много информации на одной странице. Использование Vue вместе с MVC это круто, так как мы можем загружать все наши представления оставаясь на одной странице без полной перезагрузки.

  • Для демонстрации создадим еще один контроллер, я его назвал vuerouting.
  • Создайте представление index.cshtml для этого контроллера
  • Добавьте папку “vuerouting” в папке viewmodels и файл main.js.
  • Добавьте vue-router в package.json.
  • И наконец добавьте этот код в файл main.js

  • const Vue = require("vue");
    const VueRouter = require("vue-router");
    Vue.use(VueRouter);
    
    var Foo = Vue.extend({
        template: '<p>This is foo!</p>'
    });
    
    var Bar = Vue.extend({
        template: '<p>This is bar!</p>'
    });
    
    var App = Vue.extend({});
    
    var router = new VueRouter({
        history: true,
        root: "/vue-example/vuerouting"
    });
    
    router.map({
        '/foo': {
            component: Foo
        },
        '/bar': {
            component: Bar
        }
    });
    
    router.start(App, '#app');
    

  • Также добавьте разметку в файл index.cshtml.

  • <div id="app">
        <h1>Hello App!</h1>
        <p>
            <!-- use v-link directive for navigation. -->
            <a v-link="{ path: '/foo' }">Go to Foo</a>
            <a v-link="{ path: '/bar' }">Go to Bar</a>
        </p>
        <!-- route outlet -->
        <router-view></router-view>
    </div>
    

  • После сборки проекта перейдите по такому пути: localhost/vue-example/vuerouting
  • При нажатии на ссылки будет загружаться соответствующий компонент Vue и, если вы посмотрите на адресную строку Vue добавляет маршрут к этому компоненту.

Результат может показаться очень хорошим, но это еще не все. В нас осталась проблема. Если мы скопируем наш адрес с Vue компонентом (http://localhost/vue-example/vuerouting/bar) и вставите ее в новую вкладку у нас вылезет 404 ошибка потому что сервер не в состоянии найти данный маршрут. Нам нужно настроить маршрутизацию на сервере так чтобы он игнорировал наши Vue маршруты.

  • В App_start/RouteConfig.cs перепишите код на следующий:

  • routes.MapRoute(
        name: "Default",
        url: "{controller}/{action}/{id}/",
        defaults: new {controller = "Home", action = "Index", id = UrlParameter.Optional},
        constraints:new { controller = "Home"}
        );
    routes.MapRoute(
        name: "Silo Controller",
        url: "{controller}/{*.}",
        defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional },
        constraints: new { controller = "examplevuerouter|ExampleSeedingRazor" } 
    );
    

  • Код представленный выше оставляет стандартную реализацию маршрутизации в MVC, а также добавляет альтернативную маршрутизацию для наших Vue компонентов.
  • Наш новый маршрут перенаправляет нас на Index действие указанного контроллера при этом игнорирую остальную часть маршрута.
  • После сборки проекта попробуйте перейти на localhost/vue-example/vuerouting/bar и вы увидите что страница полностью загрузилась вместе с нашим компонентом Bar.
  • Еще одна вещь, которая может быть улучшена это корневые значения в настройках маршрутизации Vue. В приведенном выше примере вам придется изменить значение маршрута если вы добавите еще один Vue модуль. Вместо этого мы можем добавить еще одну JavaScript переменную в наш макет, так как мы делали выше с serverUrl. На самом деле при изменении макета страницы проекта мы уже добавили window.VueRouterUrl. Так что нам осталось только немного поменять код:

  • var router = new VueRouter({
        history: true,
        root: "/vue-example/vuerouting"
    });
    

  • var router = new VueRouter({
        history: true,
        root: window.VueRouterUrl
    });
    

Ссылка на оригинал: тык.

Я надеюсь, что информация, которую я предоставил в данной статье поможет вам при создании ваших проектов с использованием VueJS и MVC. Если у вас есть замечания, вопросы и предложения прошу оставить их в комментариях.

Спасибо всем за прочтение.