RequireJS для приложений Vue.js + Asp.NETCore + TypeScript

  • Tutorial

logos


Создаем на 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).


В браузере должно получиться что-то подобное изображенному на скриншоте.


image


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


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. Если кто-нибудь знает более правильный способ — поделитесь.


Ссылки:



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.

Поделиться публикацией
Похожие публикации
Ой, у вас баннер убежал!

Ну. И что?
Реклама
Комментарии 11
  • 0

    default не имеет отношения к модулям AMD. Эта штука появилась в ES6-модулях и оттуда перекочевала в TypeScirpt-модули.


    На самом деле вы просто неправильно подключаете Vue. Вы используете вот такую конструкцию:


    import Vue from "vue";

    Эта конструкция использует default import, который, в свою очередь, выливается в обращение к свойству default.


    А надо делать — вот так:


    import * as Vue from "vue";
    • 0

      Ну или можно использовать файл vue.esm.js вместо vue.js: там делается default export вместо простого присваивания.

      • 0
        Спасибо. Попробую поковырять этот вариант.

        Хотя «в лоб» не проходит, при выполнении ошибка:
        Uncaught (in promise) Error: Unable to dynamically transpile ES module

        A loader plugin needs to be configured via `SystemJS.config({ transpiler: 'transpiler-module' })`.

        Instantiating cdn.jsdelivr.net/npm/vue@2.5.13/dist/vue.esm.js
        • 0
          RequireJS, гад такой, вообще отказался vue.esm.js переваривать:
          vue.esm.js:10809 Uncaught SyntaxError: Unexpected token export
        • 0
          Попробовал import * as Vue from «vue»;
          Компилятор TS выдает ошибки.
          Может подскажете, что ещё подкрутить?

          TryVue D:\Git\starter-vue\TryVue\ClientApp\index.ts 5
          Ошибка TS2351 Build: Невозможно использовать new с выражением, у типа которого нет сигнатуры вызова или конструктора.

          TryVue D:\Git\starter-vue\TryVue\ClientApp\components\Hello.ts 4
          Ошибка TS2339 Build: Свойство «extend» не существует в типе «typeof „D:/Git/starter-vue/TryVue/node_modules/vue/types/index“».

          • 0
            А, ну там еще тайпинги поменять надо…
            • 0
              Компилятор, вроде, берет родные тайпинги здесь: «node_modules/vue/types/index». Если надо что-то добавить — я готов попробовать.

              Может есть ссылка на работающий пример с TypeScript для случая: import * as Vue from «vue»?
              • 0

                Ох, там там еще и тайпинги родные?.. Значит, я невнимательно читал вашу прошлую статью...


                Вот, нашел еще одно решение. Нужна настройка "esModuleInterop": true.

                • 0

                  Сработало! Спасибо!
                  Проект с amd-модулями работает без заплатки и через SystemJS, и через RequireJS. В файлах TypeScript ничего менять не надо, достаточно поменять tsconfig.json:


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

                  Заплатку можно убирать. Получается зря гланды вырывал не через то место.

                  • 0
                    На всякий случай уточняю, что «esModuleInterop» появился в версии TypeScript 2.7. На ноуте не обновился и некоторое время не мог понять, почему работавшее приложение вдруг перестало собираться.
          • 0

            В статью внесено дополнение под заголовком UPD 01.03.2018 с учетом предложенного решения @mayorovp.


            Также все необходимые изменения внесены в решение VS2017 на github.

            Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

            Самое читаемое