логотипы

Создаем на Visual Studio 2017 модульное приложение Vue.js + Asp.NETCore + TypeScript без использования Webpack или Broserify. Причем сначала делаем проект с использованием Webpack, а потом без него. Чтобы прочувствовать, от какого счастья мы отказываемся.

Материал рассчитан на способных управиться с VS2017 и знакомых с прогрессивным JavaScript фрэймворком Vue.js.

Цель замены системы сборки – снижение стартового барьера для освоения Vue.js за счет уменьшения количества применяемых инструментов при создании современных веб-приложений.

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

Содержание

Введение

Проект TryVueWebpack
Создание стартовой болванки
Добавление программного кода
Добавление файлов конфигурации
Сборка и запуск приложения

Проект TryVue
Создание стартовой болванки
Копирование программного кода
Добавление файлов конфигурации и компиляция
Сборка шаблонов и CSS
Создание index.html и запуск приложения

Заключение

Введение


Webpack – очень мощный и полезный инструмент, который позволяет делать практически всё. Вам, рано или поздно, придется его освоить, если собираетесь заниматься веб-разработкой серьезно.

Но когда изучаешь Vue.js одновременно с TypeScript, очень хочется убрать лишних посредников. Хотя бы при освоении новых технологий и на простых задачах. Сложно разбираться c TypeScript, когда получаемый на выходе код является результатом переваривания кучей фильтров и конверторов Webpack.
«Прежде, чем объединяться, и для того, чтобы объединиться, мы должны сначала решительно и определенно размежеваться». В.И. Ленин
В нашем проекте TryVue без Webpack: большую часть работы по сборке и установлению взаимосвязей между модулями будет делать штатный компилятор TypeScript, загрузку модулей во время исполнения – System.js, конкатенацию шаблонов и файлов CSS – Bundler&Minifier (почти штатное расширение к VS2017).

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

Сначала собираем модульное приложение с использованием Webpack. Затем переводим полученное приложение с Webpack на сборку штатным компилятором TypeScript + Bundler&Minifier.

Для иллюстрации способа избавления от Webpack подготовлено решение Asp.Net Core с двумя проектами: TryVueWebpack, TryVue (исходники на gihub). Структура проекта TryVueWebpack приведена на скриншоте, что-то подобное вы получите после выполнения описанных здесь действий.

скрытый скриншот
image

Проект TryVueWebpack


При написании этого руководства использовался пример TypeScript Vue Starter от разработчиков TypeScript из компании Microsoft. Исходный код примера реструктурирован и превращен в проект Asp.Net Core на Visual Studio 2017.

Создание стартовой болванки


Для начала создаем стартовую заготовку для приложения. В качестве отправной точки используем проект «Веб-приложение ASP.NET Core» по шаблону «Пустой»

скрытый скриншот

В новом проекте создаем стартовую страницу wwwroot\index.html, в которой определяем место внедрения приложения Vue.js, а также загружаемый js-файл.

скрытый текст фрагмента wwwroot/index.html
<!-- wwwroot/index.html-->
...
<body>
    <div id="app-root">loading..</div>
    <script src="./dist/build.js"></script>
</body>


Затем обеспечиваем открытие index.html страницы при запуске приложения Asp.Net Core, изменив текст класса в файле Startup.cs.

скрытый текст фрагмента Startup.cs
// Startup.cs
...
public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
    }

    public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    {
        app.UseDefaultFiles();
        app.UseStaticFiles();
    }
}


Сейчас можно проверить, что приложение почти запускается (VS2017 запустит IIS Express, в браузере отобразится «loading...»).

Добавление программного кода


Теперь добавляем в приложение vue-компоненты и другой необходимый код для компиляции и работы приложения.

В каталоге ClientApp\components создаем 6 файлов для двух vue-компонент. Для каждой компоненты стили CSS и код TypeScript выносим в отдельные файлы, а в vue-файле оставляем только шаблон (плюс ссылки на css, ts). Однофайловые Vue-компоненты в чистом виде применять не будем (чтобы не ставить плагин для подсветки синтаксиса и т.д.).

скрытый текст ClientApp/components/Hello

<!-- ClientApp/components/Hello.vue -->
<template>
  <div>
    <div class="greeting">Hello {{name}}{{exclamationMarks}}</div>
    <button @click="decrement">-</button>
    <button @click="increment">+</button>
  </div>
</template>
<script src="./Hello.ts" lang="ts"></script>
<style src="./Hello.css"></style>

// ClientApp/components/Hello.ts
import Vue from "vue";

export default Vue.extend({
    props: ['name', 'initialEnthusiasm'],
    data() {
        return {
            enthusiasm: this.initialEnthusiasm
        }
    },
    methods: {
        increment() { this.enthusiasm++; },
        decrement() {
            if (this.enthusiasm > 1) {
                this.enthusiasm--;
            }
        },
    },
    computed: {
        exclamationMarks(): string {
            return Array(this.enthusiasm + 1).join('!');
        }
    }
});

/* ClientApp/components/Hello.css */
.greeting {
    font-size: 20px;
}

скрытый текст ClientApp/components/AppHello

<!-- ClientApp/components/AppHello.vue -->
<template>
  <div>
    Name: <input v-model="name" type="text" />
      <HelloComponent :name="name" :initialEnthusiasm="5" />
    </div>
</template>
<script src="./AppHello.ts" lang="ts"></script>
<style src="./AppHello.css"></style>

// ClientApp/components/AppHello.ts
import Vue from "vue";
import HelloComponent from "./Hello.vue";

export default Vue.extend({
    data() {
        return {
            name: "World"
        }
    },
    components: {
        HelloComponent
    }
});

/* ClientApp/components/AppHello.css */
body {
    background-color: aliceblue;
}


В каталоге ClientApp создаем файл index.ts, используемый как точка входа в приложение, а также заглушку vue-stub.ts, которая позволит компилятору TypeScript понимать, что делать с vue-модулями.

скрытый текст ClientApp\index.ts, ClientApp\vue-stub.ts
// ClientApp/index.ts
import Vue from "vue";
import AppHelloComponent from "./components/AppHello.vue";

let v = new Vue({
    el: "#app-root",
    template: '<AppHelloComponent />',
    //render: h => h(AppHelloComponent),
    components: {
        AppHelloComponent
    }
});

// vue-stub.ts
declare module "*.vue" {
    import Vue from "vue";
    export default Vue;
}


Добавление файлов конфигурации


Определяем конфигурацию NPM (менеджера пакетов Node.js), компилятора TypeScript, а также Webpack.

Добавляем в проект файл конфигурации NPM под именем package.json. Если у вас включено автоматическое обновление NPM-пакетов, учтите, что обновление может идти долго. Кроме того, возможен сбой при обновлении. В случае сбоя придется повторить восстановление пакетов, а лучше закрыть VS2017 и установить NPM-пакеты из командной строки.

скрытый текст package.json
{
  "version": "1.0.0",
  "name": "asp.net",
  "private": true,
  "scripts": {
    "build": "webpack"
  },
  "dependencies": {
    "vue": "^2.5.13"
  },
  "devDependencies": {
    "css-loader": "^0.28.9",
    "ts-loader": "^3.5.0",
    "typescript": "^2.7.2",
    "vue-loader": "^14.1.1",
    "vue-template-compiler": "^2.5.13",
    "webpack": "^3.11.0"
  }
}


Добавляем в проект файл конфигурации TypeScript под именем tsconfig.json, в котором определяются опции компилятора (compilerOptions) и каталог, в котором компилятор будет искать «свои» файлы (include).

скрытый текст tsconfig.json
{
  "compilerOptions": {
    "outDir": "./built/",
    "sourceMap": true,
    "strict": true,
    "module": "es2015",
    "moduleResolution": "node",
    "target": "es5"
  },
    "include": [
        "./ClientApp/**/*"
    ]
}


Добавляем в проект файл JavaScript под именем webpack.config.js, в котором определяются входные/выходные файлы и способы их обработки.

скрытый текст webpack.config.js
// webpack.config.js
var path = require('path')
var webpack = require('webpack')

module.exports = {
  entry: './ClientApp/index.ts',
  output: {
    path: path.resolve(__dirname, 'wwwroot/dist'),
    publicPath: 'wwwroot/dist/',
    filename: 'build.js'
  },
  module: {
    rules: [
      {
        test: /\.vue$/,
        loader: 'vue-loader',
        options: {
          loaders: {
            // Since sass-loader (weirdly) has SCSS as its default parse mode, we map
            // the "scss" and "sass" values for the lang attribute to the right configs here.
            // other preprocessors should work out of the box, no loader config like this necessary.
            'scss': 'vue-style-loader!css-loader!sass-loader',
            'sass': 'vue-style-loader!css-loader!sass-loader?indentedSyntax',
          }
          // other vue-loader options go here
        }
      },
      {
        test: /\.tsx?$/,
        loader: 'ts-loader',
        exclude: /node_modules/,
        options: {
          appendTsSuffixTo: [/\.vue$/]
        }
      },
      {
        test: /\.(png|jpg|gif|svg)$/,
        loader: 'file-loader',
        options: {
          name: '[name].[ext]?[hash]'
        }
      }
    ]
  },
  resolve: {
    extensions: ['.ts', '.js', '.vue', '.json'],
    alias: {
      'vue$': 'vue/dist/vue.esm.js'
    }
  },
  devServer: {
    historyApiFallback: true,
    noInfo: true
  },
  performance: {
    hints: false
  },
  devtool: '#eval-source-map'
}

if (process.env.NODE_ENV === 'production') {
  module.exports.devtool = '#source-map'
  // http://vue-loader.vuejs.org/en/workflow/production.html
  module.exports.plugins = (module.exports.plugins || []).concat([
    new webpack.DefinePlugin({
      'process.env': {
        NODE_ENV: '"production"'
      }
    }),
    new webpack.optimize.UglifyJsPlugin({
      sourceMap: true,
      compress: {
        warnings: false
      }
    }),
    new webpack.LoaderOptionsPlugin({
      minimize: true
    })
  ])
}


Сборка и запуск приложения


Остается проверить работоспособность сделанного. Закрываем Visual Studio и через командную строку в каталоге проекта выполняем следующее:

npm install
npm run build
dotnet run

В браузере идем по указанному адресу, например, localhost:52643. В вашем случае порт наверняка будет другим. Должны получить что-то подобное изображенному на скриншоте.

screenshot

Этот способ сборки и запуска приложения далеко не самый удобный, но его было проще описать. Запускать полученное приложение можно прямо из VS2017 (Ctrl+F5) или просто открывать в браузере файл wwwroot\index.html.

Со сборкой чуть посложнее. Если потребуется часто запускать скрипты сборки из package.json, попробуйте NPM Task Runner – достаточно популярное расширение к VS2017.

Желающие могут посмотреть внимательно на файл wwwroot\dist\build.js. Это файл у меня имеет размер 893kB, там код нашего приложения собран вместе с кодом vue.esm.js. Причем сборщик туда закопал не только наш код JavaScript, но также и CSS. Если в этом файле поставить в нужном месте точку остановки и походить отладчиком, то можно увидеть, что Webpack выполняет массу полезной работы. Обеспечивает инициализацию компонент с учетом зависимостей, кэширует вызовы и т.д.

Встроенный отладчик VS2017 на ts-файлах работать не будет, если оставить webpack.config.js без изменений. После некоторых изысканий нашел, что отладчик начинает работать если установить опцию {devtool:'#source-map'} вместо {devtool:'#eval-source-map'}. Так как этот конфиг я взял у большого гуру, а сам не хочу разбираться на что ещё влияет эта опция, то оставил исходную версию.

Проект TryVue


Теперь приступаем к созданию проекта Vue.js + Asp.NETCore + TypeScript без Webpack.

Создание стартовой болванки


Процедура создания стартовой заготовки для проекта TryVue идентична ранее описанной процедуре для проекта TryVueWebpack. Стартовую страницу wwwroot\index.html поправим позже.

Копирование программного кода


Полностью копируем папку ClientApp из сделанного в предыдущем разделе проекта. Удаляем файл ClientApp/vue-stub.ts.

Меняем расширение имени файлов *.vue -> *.html. Затем в этих файлах удаляем тэги <script /> и <style />, прописываем значения id для <template /> (внутренности шаблонов оставляем без изменений). В результате получим следующее:

<!-- ClientApp/components/AppHello.html -->
<template id="app-hello-template">
.. внутренности шаблона ..
</template>

<!-- ClientApp/components/Hello.html -->
<template id="hello-template">
.. внутренности шаблона ..
</template>

Теперь у нас «мухи — отдельно, котлеты — отдельно», в смысле: шаблоны лежат отдельно от ts-кода компонент. Поэтому в ts-коде самих компонент надо в свойство «template» прописать идентификторы своих шаблонов.

// ClientApp/components/AppHello.ts
...
export default Vue.extend({
    template: "#app-hello-template",
    ...
});

// ClientApp/Hello.ts
...
export default Vue.extend({
    template:"#hello-template",
    ...
});

Окончательно убираем следы однофайловых vue-компонент, исправив ссылки в директивах импорта файлов ClientApp/AppHello.ts, ClientApp/index.ts.

// ClientApp/components/AppHello.ts
import Vue from "vue";
import HelloComponent from "./Hello"; // было "./Hello.vue"
...

// ClientApp/index.ts
import Vue from "vue";
import AppHelloComponent from "./components/AppHello"; // было "./AppHello.vue"
...

Добавление файлов конфигурации и компиляция


Добавляем в проект файл конфигурации NPM под именем package.json. На этот раз нам достаточно указать только пакет Vue. Обычно новые NPM-пакеты после изменения в package.json устанавливаются автоматически. В противном случае — вызвать команду восстановления пакетов принудительно.

{
  "version": "1.0.0",
  "name": "asp.net",
  "private": true,
  "devDependencies": {
    "vue": "^2.5.13"
  }
}

Добавляем в проект файл конфигурации TypeScript под именем tsconfig.json и прописываем необходимые параметры.

{
  "compilerOptions": {
    "sourceMap": true,
    "target": "es5",
    "strict": true,
    "module": "system",
    "outFile": "wwwroot/dist/main.js",
    "moduleResolution": "node"
  },
  "include": [
    "./ClientApp/**/*.ts"
  ]
}

Обратите внимание на опции компилятора TypeScript {«module»: «system», «outFile»: «wwwroot/dist/main.js»}. При таких настройках, компилятор сам соберет полученный на выходе js-код всех модулей в единый файл. Причем специальные обертки этих модулей и библиотека System.js обеспечивают инициализацию модулей в нужном порядке, с учетом взаимных зависимостей.

Именно опция «module» компилятора TypeScript (при значении «amd» или «system») позволяют нам отказаться от Webpack. Посмотрите на файл wwwroot/dist/main.js после компиляции проекта со значениями {«module»: «amd»}, затем {«module»: «system»}.

Файл с результатами работы TypeScript компилятора содержит последовательность вызовов функции define() или System.register(). В параметрах вызова функций можно увидеть определение зависимости модулей друг от друга.

// wwwroot/dist/main.js при компиляции с опцией {"module": "amd"}
define("components/Hello", ["require", "exports", "vue"], function (require, exports, vue_1) {
...
});
define("components/AppHello", ["require", "exports", "vue", "components/Hello"], function (require, exports, vue_2, Hello_1) {
...
});
define("index", ["require", "exports", "vue", "components/AppHello"], function (require, exports, vue_3, AppHello_1) {
...
});

// wwwroot/dist/main.js при компиляции с опцией {"module": "system"}
System.register("components/Hello", ["vue"], function (exports_1, context_1) {
...
});
System.register("components/AppHello", ["vue", "components/Hello"], function (exports_2, context_2) {
...
});
System.register("index", ["vue", "components/AppHello"], function (exports_3, context_3) {
...
});

Если компилятор TypeScript сам способен выполнить большую часть необходимой работы, тогда «нафига козе баян»? После анализа результатов компиляции должно быть ясно, в какую сторону копать, чтобы избавиться от сторонних тяжелых сборщиков.

Сборка шаблонов и CSS


Со сборкой JavaScript в bandle разобрались. Теперь надо сделать бандлы vue-шаблонов и CSS. Для этого можно действовать совсем тупо — использовать команду copy (конкатенация — она и в Африке конкатенация):

copy ClientApp\components\*.css wwwroot\dist\main.css
copy ClientApp\components\*.html wwwroot\dist\app-templates.html

Но, боюсь, меня тут неправильно поймут. Обычно принято такие вещи делать при помощи Gulp или Grunt. Также применяется специальное расширение для Visual Studio — Bundler&Minifier. Надеюсь, с установкой и применением этого расширения справитесь. Привожу bundleconfig.json для нашего случая:

[
  {
    "outputFileName": "wwwroot/dist/main.css",
    "inputFiles": [
      "ClientApp/**/*.css"
    ]
  },
  {
    "outputFileName": "wwwroot/dist/app-templates.html",
    "inputFiles": [
      "ClientApp/**/*.html"
    ],
    "minify": {
      "enabled": false,
      "renameLocals": false
    }
  }
]

Создание index.html и запуск приложения


После выполнения описанного выше, в каталоге wwwroot\dist должны получить три бандла: main.js, main.css, app-templates.html. Остается только сделать index.html, который обеспечит их использование.

Подключаем стили, добавив в <head /> ссылку на main.css:

<link rel="stylesheet" href="dist/main.css" type="text/css" />

Загружаем файл с vue-шаблонами с самом начале <body />, затем определяем точку внедрения приложения Vue.js:

<section id="app-templates"></section>
<script src="https://code.jquery.com/jquery-3.2.1.min.js"></script>
<script>
    $.get("dist/app-templates.html").done(function (data) {
        $('#app-templates').append(data);
    });
</script>

<div id="app-root">loading..</div>

Загружаем System.js, который, в свою очередь, грузит все необходимое и стартует приложение.

<script src="https://cdnjs.cloudflare.com/ajax/libs/systemjs/0.20.19/system.src.js"></script>
<script>
    System.config({
        map: {
            "vue": "https://cdn.jsdelivr.net/npm/vue@2.5.13/dist/vue.js"
        }
    });
    SystemJS.import('dist/main.js').then(function (m) {
        SystemJS.import('index');
    });
</script>

Запуск полученного приложения в среде Visaul Studio — обычный (например: F5). Нормально работает встроенный отладчик VS2017, причем точки остановки можно ставить и в коде ts-модулей.

Заключение


Отказ от Webpack при разработке Vue.js-приложений, позволил вести разработку и отладку исключительно в привычной среде VS2017. В получении бандлов участвует только TypeScript и конкатенатор.

Для сравнения — в случае использования Webpack, в процедуре сборки участвуют, как минимум: webpack, typescript, ts-loader, css-loader, sass-loader, vue-loader, vue-style-loader, vue-template-compiler, vue-template-es2015-compiler, vue-hot-reload-api. Всего в каталоге node_modules: более 400 пакетов объемом около 80 Мб.

Правда, для production-версии полезно vue-шаблоны компилировать в рендер-функции, поэтому посредники для нашего случая тоже потребуется. Но, всё равно, без Webpack их будет значительно меньше.

Когда собираешь бандлы (main.js, main.css, app-temlates.html) без Webpack, есть понимание, из чего и как они получены. Поэтому незначительные правки можно вносить в бандлы напрямую (потом не забывать переносить в исходники). Экономия времени и нервов очень большая, особенно, при прототипировании отдельных компонент, для экспериментов и проверки идей.

Надеюсь, и вам, описанное здесь, принесет пользу.

Ссылки:

.

UPD 20.02.2018:

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


  1. apapacy
    25.02.2018 17:04

    Интересный способ. Но есть ещё вопросы как на счёт code splitting? Скомпоновать все в один бандл это не вариант уже сейчас. Иначе первая страница грузит все приложение и это долго на слабых мобильных девайсах. Webpack реализовал фичу которая ещё не попала в стандарт динамический import() который делит код на несколько бандлов автоматически или без него можно это сделать вручную


    1. edward_nsk Автор
      25.02.2018 17:24

      Так для production никто не запрещает резать бандл «правильно». У меня, например, их 4 штуки получается. Более того, на стартовой странице приложения я, вообще, хочу оставить только Bootstrap + jQuery. А грузить основную часть приложения только для желающих пойти дальше.

      Пока собираю эти бандлы при помощи Webpack, но собираюсь попробовать обойтись Gulp (vue-template-compiler + самописный конкатенатор).


      1. apapacy
        25.02.2018 21:30

        А как правильно? webpack делает это сам на основании динамических импортов (хотя есть возможность и вручную разрулить все). Какая альтернатива в Вашем решении? Есть ли возмодност сделать это автоматилированно?


        1. edward_nsk Автор
          25.02.2018 21:44

          Наверно ваш вариант с Webpack правильнее, особенно, для production. Вариант без Webpack полезен при прототипировании, отладке, изучении потрохов библиотек и т.п.


  1. k12th
    25.02.2018 17:20

    Чтобы уменьшить количество инструментов, отказываемся от webpack… в пользу SystemJS! Браво!


    1. edward_nsk Автор
      25.02.2018 18:36

      У SystemJS настроек на порядок меньше, чем у Webpack.
      Поэтому, в ряде случаев, замена на SystemJS выглядит предпочтительнее.
      Для осваивающих экосистему веб-программирования с нуля, грустно начинать с изучения Webpack, чтобы написать «Hello World!»

      Ещё отладчик VS2017 меньше глючит на сборке штатным компилятором TypeScript.


      1. k12th
        25.02.2018 19:47
        +1

        Согласен, настраивать с нуля webpack очень грустно. Но есть же vue-cli.


        Ну а кодить под веб в vs2017 — это уж, простите, ССЗБ:)


        1. edward_nsk Автор
          25.02.2018 20:07

          Куда ж крестьянину податься, который, в основном, именно на VS2017 работает. У нас приложение Asp.Net Core + WPF, а веб-интерфейс дополнительный.


          1. apapacy
            25.02.2018 21:36

            Обычно делают так: оставить на ASP только api фронтенд делать на технологиях Vue/React/Angular/Elm… Вот например интересный проект в котором одну не самую примитивную задачу решают разными технологиями в т.ч. и ASP. github.com/gothinkster/realworld. В смысле задача конечно простая но не совсем примитивная. Не ToDoMVC.


            1. edward_nsk Автор
              25.02.2018 22:38

              Спасибо, посмотрю.


        1. Dreyk
          25.02.2018 23:10

          да че там настраивать. кроме того, есть parcel, с его zero-configuration, да и webpack 4 уже тоже без конфига искаропки работает


  1. mikhailt
    26.02.2018 02:05

    А как это в продакшн деплоить? Скажем на IIS.
    Какие папки с собой тащить? Что делать с толстым node_modules?


    1. k12th
      26.02.2018 03:57

      Точно так же, как всё остальное — в собранном для продакшена виде это обычная статика. Собирать, наверное, лучше перед деплоем, и заливать только артефакты сборки.


  1. edward_nsk Автор
    26.02.2018 07:42

    Толстый node_modules для деплоя не нужен, в приведенном примере используется только для сборки. Всё необходимое в рантайм тащится по ссылкам CDN: vue.js, jquery.js, system.js. Деплой можно делать визардом vs2017.


  1. pawlo16
    28.02.2018 11:42
    -1

    Писать что-то серьезное на нетипизированном языке нельзя. Следовательно все фреймворки наподобие этого Vue со своими шаблонами (DSL, в котором строгой типизации нет, и как ее приделать — непонятно) сразу выкидываются в помойку, без попыток в них разобраться.