вступление
Fable - одна из самых захватывающих вещей, которые произошли в экосистеме F # за последние несколько лет. Это мост, который соединил разработчиков F # с огромным миром JavaScript, сделал все, что он предлагает, доступным, в то же время позволяя им писать код на их любимом языке.
Неудивительно, что к настоящему времени практически все, кто использует F # для развлечения и получения прибыли и занимается веб-разработкой, уже пробовали Fable и играли с SAFE Stack, рассказывали / писали в блогах о своем опыте, успешно использовали эти технологии в производстве, создали соответствующие инструменты. и библиотеки или способствовали им. Я тоже пробовал и играл, но пока не сделал ничего полезного для других (ни Fable, ни F # в целом). Адвент-календарь F # казался хорошей возможностью исправить это, оказав на меня реальное давление, поэтому я решил поучаствовать и сделать что-нибудь Fable.
Этот пост - история моего пути: выбор того, что делать, демонстрация результатов, процесса и извлеченных уроков. В конце я делюсь пошаговым руководством о том, как сделать что-то подобное, а также ссылками на полезные ресурсы. Итак, если вам не хочется читать длинную часть рассказа, не стесняйтесь пропустить ее и перейти к «Результаты», а затем к «Хотите сделать что-то подобное? Вот краткое руководство ’.
Выбор темы и начало работы
Чтобы немного подкрепить свой выбор. Я какое-то время интересовался компьютерной графикой, хотя и не ботанистым, зная математику, а скорее в стиле компьютерной игры с существующими инструментами: я повеселился с 3Ds MAX в В средней школе создал очень простую программу просмотра объектов сетки на C # с использованием Direct3D в качестве промежуточного проекта в колледже, а мой дипломный проект был попыткой восстановить трехмерные объекты на основе их изображений, сделанных под разными углами. После этого прошло 8 лет корпоративной веб-разработки, и трехмерное развлечение стало лишь слабым, но приятным воспоминанием. Так почему бы мне не заняться здесь чем-то связанным с компьютерной графикой? Было бы приятно сделать даже небольшой шаг в этом направлении.
Я начал поискать в Google и наткнулся на репозиторий fable-graphics, где участники создали привязки Fable на основе определений TypeScript для множества существующих библиотек CG JavaScript и портировали некоторые примеры с JS на F #. Я заметил, что репозиторий содержит привязки к Three.js, 3D-библиотеке JavaScript, но не имеет реализованных примеров для нее. Я подумал: Отлично! Затем я перенесу несколько примеров с JS на F # с помощью Fable!
Я больше смотрел на Three.js, и это поразило меня. Поскольку я какое-то время внимательно не следил за развитием компьютерной графики, у меня было лишь смутное представление о том, что возможно сейчас, в 2018 году. Ух ты! Взгляните, например, на этот образец. Или этот. Все это работает в браузере без задержек, и для создания таких вещей не нужно много кода! Мне это кажется даже более удивительным, чем Wi-Fi в самолетах.
Клонировав репозиторий, я попытался собрать и запустить код, но потерпел неудачу, а затем обнаружил, что репозиторий практически мертв. Комментарии в выпуске GitHub привели меня к новым репозиториям, одно с новыми привязками, а второе с Образцами Pixi (это аккуратная библиотека 2D-графики). Однако новых привязок Three.js не было, поэтому мне нужно было сгенерировать их самостоятельно, и это было нормально, поскольку я знал, что существует инструмент, который мог бы выполнять эту работу.
TS 2 Fable спешит на помощь! Если вы установите этот инструмент, найдите определения типов TypeScript для своей JS-библиотеки здесь, в репозитории Определенного Типа, загрузите их и загрузите в инструмент, он сгенерирует объявления интерфейса F #. Они позволят вашему коду F # понять, что эти типы существуют, и дадут достаточно знаний для Fable, чтобы он мог преобразовать ваш код F #, который использует библиотеку, в код JavaScript, который правильно ссылается на вещи из библиотеки.
Открыв Определения TS для Three.js, я немного испугался. Тонны файлов. Что делать, если ts2fable не может генерировать несколько файлов с взаимозависимостями? К счастью, может, так как не так давно. Ура!
Установив ts2fable и загрузив определения типов в папку «three», я выполнил следующую команду:
ts2fable three/index.d.ts TestCompile/ThreeJs.fs -e three
Вы, наверное, догадались, что файл index.d.ts является файлом «точки входа». В результате я получил огромный бандл-файл с 6К + строками кода F #. Я включил этот файл в тестовый проект, и он успешно скомпилировался после того, как я внес незначительное исправление, добавив «конец интерфейса» к пустым определениям интерфейсов. Довольно круто! Однако я столкнулся с проблемой при попытке запустить приложение. Ошибки были следующего вида:
FABLE: Cannot have two module members with same name: Camera
И еще много всего в этом роде. Оказалось, что это ошибка BABEL (или ограничение ¯ \ _ (ツ) _ / ¯). Я просмотрел определения типов Three.js и обнаружил, что существует модуль Core, а типы из него используются в других модулях с псевдонимами, что, по-видимому, приводит к беспорядку, когда происходит Fable - ›BABEL -› JS. Я подумал: «Хорошо, а что, если я найду простейший возможный образец и буду использовать только часть Core?» Прокомментировал все, кроме Core - нет, не будет работать, так как другие модули зависят от Core, а Core зависит от других модулей - прекрасно ! И, о, вот почему пространство имен в моем огромном сгенерированном файле определений F # объявлено как rec, что, кстати, приводит к тому, что VS Code сходит с ума и время от времени зависает (я не виню это! 6K строк, rec…). Я закончил тем, что бросил этот файл в суть, чтобы выделить синтаксис, и использовал его для поиска вещей, так как я все равно не собирался оставлять его в проекте.
Учитывая вышесказанное, моей целью в этом приключении стало следующее:
- Возьмем не самый крутой, а самый простой пример, который использует только Three.js и ничего больше, потому что время - ›0
- Глядя на код JavaScript в примере, используйте выходной файл ts2fable в качестве ссылки и найдите только те типы, которые мне нужны для образца, только с нужными мне полями и функциями, и создайте свою собственную минималистичную версию файла привязок F #
- Переносите JavaScript на F #, сохраняя все как можно ближе к исходному образцу для прозрачности, и пусть все работает.
Полученные результаты
В итоге я выбрал следующий образец: camera_array. Идея состоит в том, чтобы отобразить сцену, в которой N экземпляров 3D-объекта визуализируются в анимированной манере, поворачиваясь. Также есть базовая настройка света и фоновая текстура, которая помогает продемонстрировать тень, отбрасываемую объектом при его повороте при освещении светом.
Вывод, визуальная часть
Я немного изменил образец для развлечения. Просто изменили размеры цилиндров, чтобы они превратились в конусы (это поцелуи Херши!), И изменили используемые цвета. Это настолько "адвентский", насколько я могу его получить без использования текстур или нестандартных моделей. Конфеты и рождественские краски!
Если вы не видели эти золотые поцелуи Херши в реальной жизни, вот как они выглядят:
Вывод, часть кода
Код F # перенесенного образца был максимально приближен к исходному образцу JavaScript. Я признаю, что это не идиоматично, это в значительной степени противоположно «F # code we love» (не показывайте этот код своим детям! Пожалуйста) и содержит некоторые уловки, которые я рассмотрю в следующем разделе. Единственное, что хорошо в этом коде, так это то, что он удобен для обучения, поскольку позволяет просматривать исходный код JS, F # и сгенерированный JS и легко видеть, что происходит.
Я поместил сюда код образца: w0lya / fable-three
Проект основан на минимальном шаблоне для Fable 2, поэтому, если вы будете следовать инструкциям в шаблоне, у вас все должно работать.
А вот вывод ts2fable в виде Fable.Import.Three.fs
Процесс
После того, как я выполнил всю настройку и подготовку, сама реализация оказалась довольно простой и не такой полной приключений, как начало работы. Просто нужно было выяснить несколько вещей.
Поскольку исходная логика JavaScript реализована в императивном порядке, мне пришлось следовать этому стилю в F #, чтобы «имитировать» его.
- Первое, с чем я столкнулся, - это возможность создавать объекты с помощью конструкторов с параметрами. Если бы у меня был тип, а не определения интерфейса, это было бы довольно просто. Для интерфейсов я сделал следующее:
// Out initial type as-is. type [<AllowNullLiteral>] Vector4 = abstract x : float with get,set abstract y : float with get, set abstract z : float with get, set abstract w : float with get, set // Add this as a constructor with optional parameters: and [<AllowNullLiteral>] Vector4Type = [<Emit("new THREE.$0($1...)")>] abstract Create: ?p1:obj * ?p2:obj * ?p3:obj * ?p4:obj -> Vector4 // Further in the type definition file, add a type with global exports for 'constructing' your objects. type [<Erase>] Globals = [<Global>] static member Vector4 with get(): Vector4Type = jsNative and set(v: Vector4Type): unit = jsNative
Учитывая это, я могу написать следующий код:
subcamera.bounds <- Globals.Vector4.Create( (x / amount, y / amount, size, size )
И в результате получу следующий JavaScript, который мне и нужен:
subcamera.bounds = new THREE.Vector4(x / amount, y / amount, size, size);
Вы, наверное, заметили атрибуты [‹Emit›], [‹Erase›] и [‹Global›] в коде. Они довольно интуитивно понятны: первый переопределяет, какой JS-код создается на основе вашего F # кода, второй отмечает вещи, которые следует игнорировать при генерации JS, а третий обозначает вещи, которые должны быть глобально доступны в вашем результирующем JS. Чтобы узнать об этом больше, просмотрите ссылки, которыми я делюсь в конце сообщения.
2. Вторая вещь, которая вызвала небольшую неприятность, заключалась в том, что в исходном JS есть несколько глобальных переменных. К сожалению, мне пришлось преодолеть эту проблему, используя не самый приятный вид взлома Emit. Моя проблема заключалась в том, что если я объявлю глобальные переменные таким образом,
let globalCamera = Globals.ArrayCamera.Create()
В итоге я получаю это в JavaScript:
const globalCamera = new THREE.ArrayCamera()
Если я добавлю «изменяемый», глобальное объявление будет рассматриваться как функция, и если я попытаюсь установить для него какие-то свойства, я получу
globalCamera().propertyname = ...
чего я не хочу.
Чтобы преодолеть это, мне пришлось два раза определить глобальные вещи: один, чтобы сделать JS счастливым и на самом деле иметь там vars, а второй - сделать счастливым F #:
// 1) To actually have vars in JS [<Emit("var globalScene, globalMesh, globalCamera, globalRenderer;")>] let globalVarsDeclaration: unit = jsNative // 2) For F# not to be sad. [<Erase>] helps to exclude this stuff when generating JS. let [<Erase>] globalCamera = Globals.ArrayCamera.Create() let [<Erase>] globalScene = Globals.Scene.Create() let [<Erase>] globalRenderer = Globals.WebGLRenderer.Create() let [<Erase>] globalMesh = Globals.Mesh.Create()
С указанным выше я получаю правильное объявление, но если я ссылаюсь на них и пытаюсь установить какое-либо свойство для этих объектов, в выводе JavaScript я получаю следующее:
Object(_fable_fable_core_2_0_10_Util__WEBPACK_IMPORTED_MODULE_0__["equals"])(globalScene, scene)
И, конечно, не работает :) Это привело меня к еще более сомнительным взломам. Я объявил локальную камеру, сцену, сетку и средство визуализации внутри функции init (), в отличие от глобального, локального 'let mutable ' объявление приводит к разумному JS и позволяет устанавливать свойства по мере необходимости. Правильно настроив локальные объекты, я назначил их глобальным переменным по 2 раза каждый, один через Emit (для JS), одно простое присвоение (для F #). Я не добавляю здесь фрагменты кода для этой части, чтобы не перегружать сообщение, но если вам интересно, все это находится в файле App.fs. Хотя я почти уверен, что существует более приятный способ преодолеть эти проблемы.
Помимо этого, в определениях типов были биты, которые нужно было немного подправить, например отсутствует метод, требуются необязательные параметры или необходим базовый тип. Но в целом я смог заставить все работать относительно безболезненно.
Инструменты
Здесь я просто перечислю инструменты, которые использовал. Я все делал в Linux (Ubuntu 18.04), поэтому это повлияло на некоторые варианты.
- Код Visual Studio с Ionide
- GitKraken
- Инструменты разработчика Chromium
- Gitzip для получения небольших фрагментов больших репозиториев (например, трех привязок Typescript)
- Ts2fable, как терминальный инструмент, так и онлайн
- новый Fable 2.0 Online REPL
- Legacy Fable REPL, потому что новый REPL выводил то, что меня пугало. например это:
let returnVal$$1 = Mesh$$$$002Ector(); Mesh$$set_geometry$$Z47ACBE6C(returnVal$$1, geometry);
Однако, когда полученный в результате JavaScript стал отлаживаемым, я перестал использовать онлайн-REPL и просто переключился на отладку сгенерированного JS в VS Code с горячей перезагрузкой, что было просто отличным опытом для разработчиков. Кстати, использование Emit для вставки оператора отладчика может быть здесь полезно.
Хотите сделать что-то подобное? Вот краткое руководство
Если вы любите приключения и хотели бы портировать один из более захватывающих примеров Three.js (или сделать то же самое с любой другой библиотекой JS, в которой доступны определения TypeScript), то вот краткое изложение.
Шаги
- Найдите определения TypeScript в своей библиотеке здесь и загрузите его с помощью такого инструмента, как gitzip.
- Установите ts2fable, в директории с TS запустите команду (см. Readme для нужного для вашего случая)
- Создайте проект F #, например скопировав и используя минимальный шаблон для Fable 2. Добавьте туда файл F # TS и ссылку в .fsproj. Вы можете либо проверить правильность файла в проекте, создав и запустив его, либо бросить файл в оперативный REPL и попробовать запустить его.
- Если вам не повезло, вам придется создавать свои собственные определения типов вручную, настраивать их для поддержки конструкторов, исправлять выбросы JS и т. Д. :)
5. Не забудьте включить библиотеку JavaScript в виде пакета NuGet или ссылки на CDN.
6. Напишите свой код F # в App.fs и убедитесь, что он преобразован в рабочий JS :)
Полезные ссылки
- Взаимодействие с Javascript
- Взаимодействие F # с Javascript в Fable: Полное руководство
- Это может быть идеально для вас, если вы хотите работать с небольшой простой библиотекой: Басня для занятых мам и пап: используйте JS-библиотеку за одну минуту!
- Удивительная басня
Возможные дальнейшие действия
Возможности безграничны. Для меня или для тех, кто тоже был в восторге от этого, вот несколько идей, которые стоит рассмотреть:
- Создайте полноценное БЕЗОПАСНОЕ приложение на основе стека с потрясающими возможностями 3D.
- Добавьте взаимодействие с пользователем, сделайте приложение настраиваемым: измените количество, цвет и многое другое. Это могло бы даже стать Додзё?
- Измените логику и сделайте код F # больше похожим на код F #, который мы любим, чем на код, который нам не нравится.
- Подумайте, как преодолеть ошибку, связанную с именами Babel. как я могу автоматически повторно сгенерировать определения типов, чтобы у них не было этой проблемы, и в то же время вывод JS можно было бы использовать мгновенно, без необходимости взламывать пространство имен. Это было бы замечательно, поскольку это «разблокировало бы» всю библиотеку Three.js для использования из F #!
Конец.
Будем очень благодарны за любые отзывы / вопросы / комментарии / критику / троллинг :) Не стесняйтесь сообщать мне об ошибке здесь или в Твиттере.