Vue.js + Asp.Net Core MVC + TypeScript и ещё Bootstrap4
По стандартному шаблону Asp.Net Core MVC в Visual Studio 2017 создаем новый проект, переводим его на четвертый Bootsrtrap, встраиваем туда модульное приложение Vue.js на TypeScript.
Получаем простую, обозримую и легкую заготовку для создания своих веб-приложений на VS2017 с использованием Vue.js и TypeScript. Привычная среда разработки, в которой можно выполнять большую часть кодинга и отладки, а также быстрая пересборка приложения, делают работу вполне комфортной.
В генерации JavaScript-кода приложения принимает участие только штатный компилятор TypeScript и VS2017, что сильно сужает круг подозреваемых при возникновении глюков. А это, в свою очередь, — тоже большая экономия времени и нервов.
Материал рассчитан на способных управиться с VS2017 и знакомых с прогрессивным JavaScript фреймворком Vue.js.
Содержание
Введение
Проект TryVueMvc
— Создание стартовой болванки
— Установка NPM пакетов
— Настройка бандлинга и минификации
— Настройка компилятора TypeScript
— Создание и сборка AppHello
— Корректировка _Layout.cshtml
— Корректировка Index.cshtml
— Сборка и бандлинг из командной строки
Заключение
Ожидаемый результат выполнения данного tutorial — компонента Vue.js, внедренная на одну из страниц приложения Asp.Net Core MVC, в котором Bootstrap4 будет обеспечивать адаптивный интерфейс приложения (адаптацию под устройства с разными размерами экрана).
Сборка основного js-бандла приложения будет производится компилятором TypeScript. Сборка остальных бандлов, в том числе для библиотек внешних поставщиков, будет производится Bundler&Minifier. Попутно Bundler&Minifier минифицирует всё необходимое. Генерация html-страницы будет производится на сервере приложением Asp.NetCore MVC с использованием Razor-рендерера представлений. Загрузка и запуск js-скрипта приложения будет производится при помощи SystemJS (легко заменить на RequireJS).
Попутно настроим сборку и бандлинг приложения из командной строки через dotnet bulild
, dotnet bundle
.
Как видите, в чистом виде "сообразить на троих" не получается — каждый участник процесса норовит притащить свою компанию. Самое удачное, что получилось, хотя бы на время, избавится от Webpack, который тащил за собой много чего.
В этом tutorial сделаем порядком надоевшее приложение AppHello. В дальнейшем его можно будет заменить на своё. Для этого будет достаточно заменить ts-файлы, html-шаблоны и css-файлы.
Для данной статьи на github размещено решение VS2017 с проектом TryVueMvc. В это решение планируется добавление других проектов — просьба не обращать на них внимания.
Создание стартовой болванки
Для начала создаем стартовую заготовку для приложения. В качестве отправной точки используем проект «Веб-приложение ASP.NET Core» по шаблону MVC (модель-представление-контроллер).
Содержимое wwwroot/ надо очистить, оставить только favicon.ico. На момент написания этой статьи стандартный шаблон приложения Asp.Net Core MVC шел вместе с Bootstrap3, поэтому в каталоге wwwroot/ будут файлы используемых библиотек. Они нам не потребуются, т.к. Bootstrap мы будем устанавливать в node_modules, затем перенесем необходимое в wwwroot/.
Установка NPM пакетов
Определяем конфигурацию NPM (менеджера пакетов Node.js). Для этого добавляем в проект файл конфигурации NPM под именем package.json.
Нам нужны пакеты Vue, SystemJS и Bootstrap4. Последний, в свою очередь, требует jQuery и Popper.js.
{ "version": "1.0.0", "name": "asp.net", "private": true, "dependencies": { "jquery": "^3.3.1", "popper.js": "^1.12.9", "bootstrap": "^4.0.0", "vue": "^2.5.13", "systemjs": "^0.21.0" }
}
Обычно новые NPM-пакеты после изменения в package.json устанавливаются автоматически. В противном случае — вызвать команду восстановления пакетов принудительно.
Настройка бандлинга и минификации
В создаваемом приложении отказываемся от использования CDN (Content Delivery Network или Content Distribution Network), используемые библиотеки внешних поставщиков собираем в бандлы, минифицируем и помещаем в wwwroot/dist/.
Внешние библиотеки разбиваем на 2 части: vendor1 и vendor2 (он же vue). Сборку app-bandle.js делает TypeScript, поэтому здесь его только минифицируем.
Файлы бандлов | Источники | minfy |
---|---|---|
vendor1.js +vendor1.min.js |
node_modules/jquery/dist/jquery.js, node_modules/popper.js/dist/umd/popper.js, node_modules/bootstrap/dist/js/bootstrap.js, node_modules/systemjs/dist/system.src.js |
true |
vendor1.css | node_modules/bootstrap/dist/css/bootstrap.css | false |
vendor1.min.css | node_modules/bootstrap/dist/css/bootstrap.min.css | false |
vendor2.js +vendor2.min.js |
node_modules/vue/dist/vue.js | true |
app-bandle.min.js | wwwroot/dist/app-bandle.js | true |
app-templates.html | ClientApp/**/*.html | false |
main.css +main.min.css |
ClientApp/**/*.css | true |
Создаем пустую папку ClientApp, т.к. она указывается в bundleconfig.json (иначе будет ругань). Файл bundleconfig.json уже должен быть в проекте, остается его только правильно настроить.
[ { "outputFileName": "wwwroot/dist/vendor1.js", "inputFiles": [ "node_modules/jquery/dist/jquery.js", "node_modules/popper.js/dist/umd/popper.js", "node_modules/bootstrap/dist/js/bootstrap.js", "node_modules/systemjs/dist/system.src.js" ], "minify": { "enabled": true, "renameLocals": true }, "sourceMap": true }, { "outputFileName": "wwwroot/dist/vendor1.css", "inputFiles": [ "node_modules/bootstrap/dist/css/bootstrap.css" ], "minify": { "enabled": false } }, { "outputFileName": "wwwroot/dist/vendor1.min.css", "inputFiles": [ "node_modules/bootstrap/dist/css/bootstrap.min.css" ], "minify": { "enabled": false } }, { "outputFileName": "wwwroot/dist/vendor2.js", "inputFiles": [ "node_modules/vue/dist/vue.js" ], "minify": { "enabled": true, "renameLocals": true }, "sourceMap": true }, { "outputFileName": "wwwroot/dist/main.css", "inputFiles": [ "ClientApp/**/*.css" ], "minify": { "enabled": true } }, { "outputFileName": "wwwroot/dist/app-bandle.min.js", "inputFiles": [ "wwwroot/dist/app-bandle.js" ], "minify": { "enabled": true, "renameLocals": true } }, { "outputFileName": "wwwroot/dist/app-templates.html", "inputFiles": [ "ClientApp/**/*.html" ], "minify": { "enabled": false, "renameLocals": false } }
]
После сохранения изменений bundleconfig.json, в каталоге wwwroot/dist должны появиться бандлы от vendor1 и vendor2. Бандлы нашего приложения появятся после создания необходимых исходных файлов.
Настройка компилятора TypeScript
В свойствах проекта VS2017 есть закладка "Сборка TypeScript", в которой через удобный интерфейс можно задавать большую часть необходимых свойств компилятора. Но часть свойств компилятора, всё равно, придется определять и править в файле TryVueMvc.csproj руками. Желающие могут использовать этот способ настройки компилятора TypeScript.
Если параллельно планируется использовать Webpack или другие системы сборки, то лучше использовать tsconfig.json. Добавляем этот файл и настраиваем по приведенному образцу.
{ "compilerOptions": { "noImplicitAny": false, "noEmitOnError": true, "removeComments": false, "sourceMap": true, "target": "es5", "module": "system", "outFile": "wwwroot/dist/app-bandle.js", "moduleResolution": "node", "esModuleInterop": true }, "include": [ "./ClientApp/**/*.ts" ]
}
Создание и сборка AppHello
В нашем примере используется вариант организации проекта, когда CSS-файлы не являются собственностью отдельных компонент, а определяются для всего приложения централизованно.
Создаем папку ClientApp/css для общих css-файлов. Создаем папку ClientApp/components для ts-файлов и html-шаблонов компонент. В каталоге ClientApp создаем файл index.ts, используемый как точка входа в приложение. Добавляем остальные файлы клиентского приложения AppHello.
// ClientApp/index.ts
import Vue from "vue";
import AppHelloComponent from "./components/AppHello"; let v = new Vue({ el: "#app-root", template: '<AppHelloComponent />', //render: h => h(AppHelloComponent), components: { AppHelloComponent }
});
// ClientApp/components/AppHello.ts
import Vue from "vue";
import HelloComponent from "./Hello"; export default Vue.extend({ template:'#app-hello-template', data() { return { name: "World" } }, components: { HelloComponent }
});
<!-- ClientApp/components/AppHello.html -->
<template id="app-hello-template"> <div> Name: <input v-model="name" type="text" /> <hello-component :name="name" :initial-enthusiasm="5" /> </div>
</template>
// ClientApp/components/Hello.ts
import Vue from "vue"; export default Vue.extend({ template:'#hello-template', 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.html -->
<template id="hello-template"> <div> <div class="greeting">Hello {{name}}{{exclamationMarks}}</div> <button @click="decrement">-</button> <button @click="increment">+</button> </div>
</template>
/* ClientApp/css/site.css */
body { margin: 5rem; background-color: honeydew;
}
После сборки проекта и UpdateBundle в каталоге wwwroot/dist должны появиться готовые к использованию файлы: app-bandle.js, app-templates.html, main.css.
Корректировка _Layout.cshtml
Чтобы использовать Boostrap 4 вместо 3, надо маленько подправить содержимое файла Views/Shared/_Layout.cshtml. В качестве образца используем шаблон Bootstrap StarterTemplate с офицального сайта продукта. Результат скрещивания _Layout.cshtml с этим шаблоном приведен под спойлером.
<!DOCTYPE html>
<html>
<head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>@ViewData["Title"] - TryVueMvc</title> <environment include="Development"> <link rel="stylesheet" href="~/dist/vendor1.css" /> <link rel="stylesheet" href="~/dist/main.css" asp-append-version="true" /> </environment> <environment exclude="Development"> <link rel="stylesheet" href="~/dist/vendor1.min.css" /> <link rel="stylesheet" href="~/dist/main.min.css" asp-append-version="true" /> </environment>
</head>
<body> <nav class="navbar navbar-expand-md navbar-dark bg-dark fixed-top"> <a class="navbar-brand" asp-area="" asp-controller="Home" asp-action="Index">Logo</a> <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarsDefault" aria-controls="navbarsDefault" aria-expanded="false" aria-label="Toggle navigation"> <span class="navbar-toggler-icon"></span> </button> <div class="collapse navbar-collapse" id="navbarsDefault"> <ul class="navbar-nav mr-auto"> <li class="nav-item"><a class="nav-link" asp-area="" asp-controller="Home" asp-action="Index">Home</a></li> <li class="nav-item"><a class="nav-link" asp-area="" asp-controller="Home" asp-action="About">About</a></li> <li class="nav-item"><a class="nav-link" asp-area="" asp-controller="Home" asp-action="Contact">Contact</a></li> </ul> </div> </nav> <main role="main" class="container"> @RenderBody() <hr /> <footer> <p>© 2018 - Company</p> </footer> </main> <environment include="Development"> <script src="~/dist/vendor1.js"></script> </environment> <environment exclude="Development"> <script src="~/dist/vendor1.min.js"></script> </environment> @RenderSection("Scripts", required: false)
</body>
</html>
Приложение Asp.Net Core MVC использует движок Razor для генерации html представлений. Обратите внимание, что попутно реализовали возможность переключения на минифицированные бандлы через конфигурацию окружения (свойство ASPNETCORE_ENVIRONMENT). Пример условной генерации ссылок на требуемые бандлы приведен ниже:
... <environment include="Development"> <link rel="stylesheet" href="~/dist/vendor1.css" /> <link rel="stylesheet" href="~/dist/main.css" asp-append-version="true" /> </environment> <environment exclude="Development"> <link rel="stylesheet" href="~/dist/vendor1.min.css" /> <link rel="stylesheet" href="~/dist/main.min.css" asp-append-version="true" /> </environment>
...
Корректировка Index.cshtml
В нашем примере используется вариант веб-приложения, которое на старте может обойтись без Vue.js. Обратите внимание, что в _Layout.cshtml нет загрузки основного бандла приложения и библиотек Vue.js. Компоненты Vue.js используются только на странице Index.cshtml. Если переместить компоненты Vue.js со стартовой страницы, можно сильно уменьшить время старта приложения.
@* Views/Home/Index.cshtml *@
@using Microsoft.AspNetCore.Hosting
@inject IHostingEnvironment hostingEnv
@{ var vueUrl = hostingEnv.IsDevelopment() ? "dist/vendor2.js" : "dist/vendor2.min.js"; var mainUrl = hostingEnv.IsDevelopment() ? "dist/app-bandle.js" : "dist/app-bandle.min.js"; ViewData["Title"] = "TryVueMvc Sample";
}
<section id="app-templates"></section>
<div id="app-root">loading..</div>
@section Scripts{ <script> System.config({ map: { //"vue": "dist/vendor2.js" "vue": "@vueUrl" } }); $.get("dist/app-templates.html").done(function (data) { $('#app-templates').append(data); SystemJS.import('@mainUrl').then(function (m) { SystemJS.import('index'); }); }); </script>
}
}
Текст Views/Home/Index.cshtml должен быть понятен для работающих с Asp.Net Core. На всякий случай объясню поподробнее.
При помощи механизма DI (Dependency Injection) добираемся до свойств окружения и определяем, какие бандлы будем использовать.
@using Microsoft.AspNetCore.Hosting
@inject IHostingEnvironment hostingEnv
@{ var vueUrl = hostingEnv.IsDevelopment() ? "dist/vendor2.js" : "dist/vendor2.min.js"; var mainUrl = hostingEnv.IsDevelopment() ? "dist/app-bandle.js" : "dist/app-bandle.min.js"; ViewData["Title"] = "TryVueMvc Sample";
}
...
Определяем место вставки бандла с vue-шаблонами, а также точку внедрения приложения Vue.js:
<section id="app-templates"></section>
<div id="app-root">loading..</div>
Загружаем бандл с vue-шаблонами и вставляем в секцию #app-templates. Затем загружаем System.js, который, в свою очередь, грузит все необходимое и стартует js-скрипт приложения.
...
<script> System.config({ map: { //"vue": "dist/vendor2.js" "vue": "@vueUrl" } }); $.get("dist/app-templates.html").done(function (data) { $('#app-templates').append(data); SystemJS.import('@mainUrl').then(function (m) { SystemJS.import('index'); }); });
</script>
...
Приложение полностью готово, можно запустить стандартным для VS2017 способом.
Сборка и бандлинг из командной строки
При отсутствии потребности в пересборке приложения через командную строку, можно пропустить этот пункт. Возможно, даже стоит его пропустить — бывают глюки с пересборкой в среде VS2017, если не учитывать некоторые особенности VS2017.
Для созданного "с нуля" проекта, команда dotnet build
соберет только DLL, а компилятор TypeScript вызван не будет. Естественно, команда dotnet
совсем ничего не знает про расширение Bundler&Minifier.
Поэтому надо установить пару NuGet пекетов: "Microsoft.TypeScript.MSBuild", "BundlerMinifier.Core". Затем в файл TryVueMvc.csproj внести изменения, которые требует официальная документация на эти продукты.
<!-- фрагмент TryVueMvc.csproj -->
<Project Sdk="Microsoft.NET.Sdk.Web"> ... <ItemGroup> <DotNetCliToolReference Include="BundlerMinifier.Core" Version="2.6.362" /> </ItemGroup> <Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\TypeScript\Microsoft.TypeScript.targets" Condition="Exists('$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\TypeScript\Microsoft.TypeScript.targets')" />
</Project>
Теперь сборка и запуск приложения может производится не только в среде VS2017, но и через командную строку в каталоге проекта TryVueMvc:
npm install
dotnet build
dotnet bundle
dotnet run
Vue.js позиционируется как очень простой для освоения фреймворк. Утверждается, что кривая обучения у конкурентов значительно круче. С моей точки зрения, это действительно так.
Разве что, приходится туго тем, кто использует Asp.Net Core и VS2017 для создания приложений Vue.js на TypeScript. Реально полезных примеров и статей в интернете не много для этого сочетания используемых продуктов и технологий.
Надеюсь, что данный tutorial поможет снизить стартовый барьер при создании приложений Vue.js + Asp.Net Core MVC + TypeScript.
Анонс продолжения
Планирую опубликовать ещё один tutorial, в котором AppHello заменим на AppGrid, созданное на основе примера Grid Component Example с официального сайта Vue.js. Попутно увидим, что надо выкидывать, при замене на своё приложение.
По возможности, постараюсь описать применяемый мной вариант решения проблемы строгой типизации при использовании TypeScript (без декораторов и vue-class-component).
Благодарности
- Идея заглавной картинки и надписи отсюда.
- Фото для заглавной картинки отсюда.
- Частично использовался шаблон Bootstrap StarterTemplate с офицального сайта Bootstrap.
Ссылки