Создаем на Visual Studio 2017 модульное приложение Vue.js + Asp.NETCore + TypeScript. В качестве системы сборки вместо Webpack используем компилятор TypeScript + Bundler&Minifier (расширение к VS2017). Загрузку модулей приложения в рантайм обеспечивает SystemJS или RequireJS. Рассматриваем формат модулей AMD (asynchronous module definition), который понимает не только SystemJS, но и RequireJS.
Предупреждаю сразу — Vue.js не совсем поддерживает AMD или содержит баг, поэтому применен почти хакерский прием, он не всем подойдет. Но надеюсь, данная статья позволит вам лучше понимать, как устроен этот мир Vue.js.
Данная статья является дополнением к tutorial: Приложение Vue.js + Asp.NETCore + TypeScript без Webpack. Где в примерах использовался формат модулей SYSTEM. Делать ставку только на загрузчик SystemJS, как то, боязно. На момент написания статьи SystemJS имеет релиз 0.20, что означает вероятнось радикальных изменений в API, опциях и т.д.
Цель применения формата модулей AMD и загрузчика RequireJS – страховка от радикальных изменений в SystemJS, обеспечение возможности использования более популярного загрузчика RequireJS и формата модулей AMD.
Материал рассчитан на способных управиться с VS2017 и знакомых с прогрессивным JavaScript фрэймворком Vue.js.
Введение
Изначально материал, который касается AMD и RequireJS, был в статье: Приложение Vue.js + Asp.NETCore + TypeScript без Webpack. Но она получалась слишком большая, поэтому пришлось отрезать кусочек. Считаю, что для полноты картины, полезно разобраться также с AMD и RequireJS.
Переход на AMD
Компилятор TypeScript собирает все модули в единственный выходной файл, если установлена опция {«module»: «system»} или {«module»: «amd»}. С опцией «system» получилось, надо теперь попробовать «amd». Без этого RequireJS применить не получится, т.к. этот загрузчик понимает только amd-формат модулей.
Стартовое приложение
В качестве отправной точки, можно взять на github проект TryVue из решения Visual Studio 2017, которое было описано в основной статье. Дальнейшие действия можно выполнять в этом проекте, но лучше создать копию под именем TryVueRequire.
После сборки приложения и бандлов в каталоге wwwroot\dist должны появиться файлы: main.js, main.css, app-templates.html. Запускаем приложение любым удобным для вас способом (F5, Ctrl-F5 в среде VS2017, или снаружи: dotnet run).
В браузере должно получиться что-то подобное изображенному на скриншоте.
Напоминаю, что браузер кэширует файлы вашего приложения, поэтому надо обновлять страницу правильно (со сбросом кэша). Если заподозрите что-то неладное при дальнейших эксперименах, сбросьте кэш браузера.
Error: vue_3.default is not a constructor
Теперь в файле tsconfig.json меняем опцию компилятора на {«module»: «amd»}, пересобираем приложение и пытаемся запуститься.
Должны увидеть в браузере текст "loading..", а в консоли — ошибку, если переключиться в режим разработчика (для Chrome — клавиша F12).
Путем удаления некоторых фрагментов кода в wwwroot\dist\main.js, можно быстро выяснить как ругаются и на что именно. Привожу выдержки и файла main.js со строками, на которых происходит сбой, а также тексты ошибок:
define("components/Hello", ..., function (...) {
...
exports.default = vue_1.default.extend({
...
});
define("components/AppHello", ..., function (...) {
...
exports.default = vue_2.default.extend({
...
});
define("index", ..., function (...) {
...
var v = new vue_3.default({
...
});
Uncaught (in promise) Error: Cannot read property 'extend' of undefined
Uncaught (in promise) Error: vue_3.default is not a constructor
Первая ошибка относится к vue_1.default.extend, vue_2.default.extend. Вторая ошибка относится к vue_3.default. Из текста ошибок понятно, что vue.js не определяет конструктор "default". В этом убедиться очень просто — удалите этот default после точки у vue_1, vue_2, vue_3.
Приложение заработает! Остается разобраться, что делать с неопределенным свойством default у экземпляров Vue.js.
Заплатка для SystemJS
Наверняка, удалять "default" из файла wwwroot\dist\main.js руками после каждой сборки проекта — не самый удобный вариант. Поэтому сделаем заплатку в файле wwwroot\index.html, которая выполняет следующее: грузит vue, прописывает у него свойство default, грузит main.js и запускает приложение.
<!--фрагмент wwwroot\index.html-->
<!-- исходный фрагмент: -->
SystemJS.import('dist/main.js').then(function (m) {
SystemJS.import('index');
});
<!-- заменить на следующий: -->
SystemJS.import('vue').then(function (m) {
if (!m.default) {
m.default = m;
console.warn('HACK: vue.default was undefined');
}
SystemJS.import('dist/main.js').then(function (m) {
SystemJS.import('index');
});
});
Приложение должно заработать. После проверки этого варианта, сохраняем index.html на память в index-system.html.
Переход на RequireJS
При amd-формате модулей использование RequireJS отличается от SystemJS только способом конфигурирования и API (aplication program interface).
Для перехода на RequireJS изменения производятся исключительно в файле wwwroot\index.html. Исходные файлы кода TypeScript, а также настройки проекта не трогаем.
Простой вариант заплатки для RequireJS
Перед тем, как предложить ещё один вариант определения default-конструктора Vue.js, сделаем полный аналог index-system.html, используя RequireJS. В файле wwwroot\index.html достаточно поменять загрузку скрипта system.js -> require.js. Затем сконфигурировать require.js, загрузить необходимое и запустить приложение. Также повторяем код прописывания default при отсутствии.
<!--фрагмент wwwroot\index.html-->
...
<script src="http://cdnjs.cloudflare.com/ajax/libs/require.js/2.3.5/require.js"></script>
<script>
require.config({
paths: {
"vue": "https://cdn.jsdelivr.net/npm/vue@2.5.13/dist/vue",
"index": "dist/main"
}
});
require(['vue'], function (m) {
if (m.default === undefined) {
m.default = m;
console.log('HACK: ' + m.name + '.default was undefined');
}
require(['index']);
});
</script>
...
Сохраняем wwwroot\index.html на память в файл index-require.html, чтобы реализовать в index.html более "правильный" вариант решения проблемы неопределенного default-конструктора.
Переопределение обращений к Vue.js
Есть еще один вариант решения проблемы отсутствия свойства default у экземпляров Vue. До определения модулей в main.js, определяем маленький переходник, который все обращения к "vue" замыкает на себя и делает скорректированный экспорт "vue-parent" (переименованный истинный "vue").
Собственно определение переходника:
define("vue", ["require", "exports", "vue-parent"], function (require, exports, vueParent) {
Object.defineProperty(exports, "__esModule", { value: true });
exports.default = vueParent.default || vueParent;
});
Часть текста wwwroot\index.html, относящаяся к использованию переходника, приведена ниже. Для упрощения данного примера текст переходника включен в index.html. Правильнее держать его как отдельный файл vue-stub.js, который приклеивать в начало main.js конкатенатором.
<!--фрагмент wwwroot\index.html-->
...
<script src="http://cdnjs.cloudflare.com/ajax/libs/require.js/2.3.5/require.js"></script>
<script>
// vue-stub.js
define("vue", ["require", "exports", "vue-parent"], function (require, exports, vueParent) {
"use strict";
//if (!vueParent.default) console.warn('HACK: vue.default was undefined');
Object.defineProperty(exports, "__esModule", { value: true });
exports.default = vueParent.default || vueParent;
});
</script>
<script>
require.config({
paths: {
"vue-parent": "https://cdn.jsdelivr.net/npm/vue@2.5.13/dist/vue",
"index": "./dist/main"
}
});
require(['index']);
</script>
...
Заключение
Возможно, я чего-то не так понимаю, но библиотека vue.js в экземплярах Vue не предоставляет определение свойства "default", которое от него ожидают в модулях формата AMD (asynchronous module definition). Честно говоря, баг это или фича — не знаю.
Во такие пирожки с котятами.
Чтобы использовать amd-формат модулей и RequireJS пришлось вставлять свой переходник, в котором определять default. Если кто-нибудь знает более правильный способ — поделитесь.
Ссылки:
- Основная статья: Приложение Vue.js + Asp.NETCore + TypeScript без Webpack.
- Мой пример на github.
- При создании КДПВ (картинки для привлечения внимания) использованы логотипы официальных сайтов продуктов: vuejs.org, docs.microsoft.com, typescriptlang.org, webpack.js.org, requirejs.org.
UPD 01.03.2018:
@mayorovp предложил более правильное решение проблемы с amd-модулями: в tsconfig.json добавить опцию компилятора {"esModuleInterop": true}.
После этого переходник становится не нужен, а использование RequireJS становится тривиальным. Поэтому загрузка и запуск приложения будет выглядеть следующим образом:
<!--фрагмент wwwroot\index.html-->
...
<script src="http://cdnjs.cloudflare.com/ajax/libs/require.js/2.3.5/require.js"></script>
<script>
require.config({
paths: {
"vue": "https://cdn.jsdelivr.net/npm/vue@2.5.13/dist/vue",
"index": "./dist/main"
}
});
require(['index']);
</script>
...
Все необходимые изменения внесены в решение VS2017 на github.
Update 05.06.2019:
На данный момент исходный код примеров на на github немного отличается от приведенного в статье. Изменения вызваны обновлением версий используемых компонент (переход на Asp.NETCore 2.2 и т.д.).