Pull to refresh

Приложение Vue.js + Asp.NETCore + TypeScript без Webpack

Reading time 12 min
Views 19K
логотипы

Создаем на 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, есть понимание, из чего и как они получены. Поэтому незначительные правки можно вносить в бандлы напрямую (потом не забывать переносить в исходники). Экономия времени и нервов очень большая, особенно, при прототипировании отдельных компонент, для экспериментов и проверки идей.

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

Ссылки:

.

Update 21.03.2018:

Другие статьи по этой теме:


Update 05.06.2019:

На данный момент исходный код примеров на github немного отличается от приведенного в статье. Изменения вызваны обновлением версий используемых компонент (переход на Webpack 4.32.2, Asp.NETCore 2.2 и т.д.).
Tags:
Hubs:
+13
Comments 15
Comments Comments 15

Articles