F # Взаимодействие с Javascript в Fable: Полное руководство

У Fable, компилятора F # в Javascript, всегда был девиз: «Компилятор, генерирующий JavaScript, которым можно гордиться». Это правда, сгенерированный код javascript читабелен и идиоматичен, иногда - не могу поверить, что говорю это о Javascript - это даже красиво. Однако у Fable есть еще одна потрясающая особенность, о которой и пойдет речь в этой статье: простое взаимодействие с экосистемой Javascript.

Взаимодействие с Javascript означает, что вы должны написать код F #, который вызывает собственные функции javascript. В более общем смысле он позволяет вам генерировать настраиваемый код javascript, которым вы управляете. Также он позволяет взаимодействовать и использовать код javascript и библиотеки в вашем коде.

В этой статье я представлю вам множество примеров того, как и когда использовать взаимодействие с Javascript. Я буду рассказывать о многом, так что возьмите себе чашку кофе и приступим.

Настройка среды

Пропустите этот раздел, если вы уже знаете, как настроить минимальное приложение Fable.

Мы будем использовать местную среду разработки. Для начала вам понадобится последняя версия dotnet cli, Node и npm, установленная на вашем компьютере. Npm поставляется вместе с Node, поэтому вам не нужна отдельная загрузка.

Давайте создадим каталог для нашего проекта:

mkdir FableInterop
cd FableInterop

Если вы впервые работаете с Fable, вам нужно запустить это один раз:

dotnet new -i Fable.Template::*

Он загрузит последний проект шаблона fable с Nuget и кэширует его на вашем компьютере.

Теперь внутри каталога FableInterop вы можете запустить dotnet new fable, который сгенерирует минимальное приложение-басню из шаблона. Что касается редактора, я использую VS Code с Ionide для кроссплатформенной разработки на F #.

После того, как у вас есть проект, вам нужно установить его зависимости, это может занять пару минут (по крайней мере, на моей машине):

npm install
dotnet restore

Когда это закончится, у вас должны появиться всплывающие подсказки и автозаполнение внутри VS Code.

Работа с Fable немного сложна, вам нужно, чтобы две вещи работали одновременно для потока «редактировать-сохранить-перекомпилировать», я обычно использую 2 вкладки оболочки: одна для запуска сервера компиляции Fable с использованием dotnet fable start, Fable работает за локальным сервером, чтобы Состояние компиляции кеша, когда он перекомпилирует проект после изменения файла, ускоряет последующие компиляции. Вторая вкладка предназначена для запуска сервера разработки Webpack с использованием npm run start, который фактически отслеживает изменения в вашем проекте и отправляет их на сервер Fable. Webpack также отвечает за обслуживание вашего статического контента, объединение вашего кода и обновление браузера при успешной перекомпиляции.

Работа с кодом

Внутри src/App.fs вы можете удалить все и оставить это:

Не закрывайте Fable.Core и Fable.Core.JsInterop, поскольку они предоставляют функции и атрибуты для взаимодействия. Fable.Import.Broweser открыт только для использования console.log, и обычно рекомендуется полностью квалифицировать модуль Browser, если вы хотите использовать функцию оттуда, то есть Browser.console.log, здесь я использую его, как указано выше, для краткости.

Пока мы будем использовать только src/App.fs. Чтобы запустить режим разработки, вы можете выполнить команду:

dotnet fable npm-run start

Это запустит два процесса, один для сервера компиляции Fable и один для сервера разработки Webpack, они будут работать вместе, когда вы внесете изменения в свой код для быстрой перекомпиляции. В качестве альтернативы вы можете запускать каждый сервер отдельно: запустите dotnet fable start и на вкладке оболочки, а на другой вкладке запустите npm run start. Перейдите к http: // localhost: 8080 и откройте консоль браузера, вы должны увидеть это:

С этого момента вы можете просто отредактировать и сохранить свой App.fs файл, а затем вернуться в браузер или консоль и увидеть изменения.

The [<Emit>] Attribute

Это атрибут, который вы можете прикрепить к значениям, функциям, членам и другим языковым конструкциям, чтобы переопределить их генерацию кода по умолчанию. Далее приведены некоторые из множества вариантов использования этого атрибута.

Определение неопределенного

В Javascript есть буквальное значение, известное как undefined. В F # такой конструкции нет. Мы будем использовать атрибут Emit для генерации этого значения в следующем примере:

Обратите внимание на части: [<Emit("undefined")>], obj и jsNative. Строка «undefined» в [<Emit>] называется «выражением испускания», это то, что вводится вместо значения undefined при компиляции кода. obj - это тип, который вы придаете значению. В этом случае obj правильно, потому что undefined может быть любым объектом в javascript. Правая часть присваивания игнорируется во время компиляции из-за наличия атрибута [<Emit>]. Если этот атрибут не указан, jsNative выдаст ошибку.

Таким образом вы можете определять пользовательские значения с их собственными типами и регулярно использовать их в коде fsharp. Чтобы пояснить концепцию, вот еще один пример:

Когда компилятор встречает значение one, он просто вставляет буквальное значение в [<Emit>]. В данном случае это 1. Еще одна вещь, на которую следует обратить внимание, - это тот факт, что у нее был тип int, это, в свою очередь, позволяет мне использовать оператор +, потому что средство проверки типов считает, что это целое число, что в данном случае является правильным типом.

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

Создание буквального кода (как в примерах выше) имеет свое применение, но не очень полезно. Самое интересное начинается, когда вы параметризуете выражение emit: используете [<Emit>] с функциями, придавая им поведение, подобное макросу!

Параметризованный [‹Emit›] с функциями

Чтобы расширить наш первый пример, я хочу написать функцию, которая проверяет, равно ли значение undefined, вот пример:

Теперь посмотрим на функцию isUndefined. У него есть один параметр, называемый x общего типа 'a. Функция возвращает bool. Затем обратите внимание на $0 в выражении emit, которое является заполнителем любого значения, которое вы передаете функции вместо параметра x. Он имеет номер 0, потому что параметры имеют нулевой индекс, и поэтому первый параметр (в данном случае x) будет иметь индекс 0. Также разрешены несколько параметров (пример):

Другой полезный пример - это когда вы хотите проверить, не является ли значение числом (NaN), используя встроенную функцию isNaN:

Возможно, вы захотите вызвать функцию без параметров и получить результат, например, если вы хотите случайное число с использованием собственного Math.random():

На самом деле System.Random поддерживается Fable, так что вы тоже можете его использовать. Здесь я просто показываю вам, на что вы способны.

Типобезопасные функции Javascript с опцией ‹’ t ›

Тип Option<'a> специально используется с Fable, а именно, он стирается при компиляции. Some x становится просто x, а None становится null. Мы будем использовать это, чтобы обеспечить безопасность типов для собственных функций. Например, функция parseFloat имеет тип string -> float. parseFloat может не выполнить синтаксический анализ входной строки и вернуть NaN, я знаю, что NaN является допустимым значением для float, но ради лучшей семантики мы хотим использовать тип: string -> Option<float> и возвращать None, когда возвращаемое значение - NaN. Мы можем обернуть нативную функцию внутри типизированной и использовать сопоставление с образцом:

Выражение [<Emit>] для parseFloat следует логике: parseFloat может вернуть NaN, поэтому я проверяю, равен ли результат синтаксического анализа NaN, и возвращаю null, если это так, и в противном случае возвращаю проанализированное значение. Предоставление этой функции возвращаемого типа float option упрощает работу с такими функциями, таким образом я гарантирую, что мой код должен учитывать сбой синтаксического анализа, и обязательно обрабатывать случай None.

Однако этот подход все еще не очень надежен, потому что синтаксический анализ успешен с вводом «5x» и возвращает 5 вместо того, чтобы терпеть неудачу, как должен. Это скорее ограничение самой нативной функции, и для правильного анализа чисел мы воспользуемся небольшим трюком с javascript, чтобы сделать это. Использование оператора + перед строкой приведет к преобразованию строки в число! Я слышал, как вы говорите, почему это работает? Я не знаю, ваше предположение так же хорошо, как и мое, надеюсь, есть веские причины:

Обратите внимание, что я дважды использовал + в этом выражении emit и дважды передал параметр, что не очень эффективно, если мой параметр был результатом дорогостоящей операции. Он должен быть заключен в лямбду (для правильного определения области видимости) и использоваться только один раз:

Вы по-прежнему можете использовать функции синтаксического анализа из BCL, такие как System.Int32.(Try)Parse, System.Double.(Try)Parse и т. Д. Они реализованы таким образом, чтобы максимально имитировать реальное поведение .NET.

Написание привязки JQuery - это просто клей.

Мы будем использовать то, что мы узнали до сих пор, для написания привязки jQuery. Привязка - это набор функций, которые вызывают функции исходного API. В этом случае собственная библиотека - JQuery.

Прежде всего, добавьте ссылку на JQuery в ваш public/index.html файл с тегом скрипта и вставьте его перед в ваш bundle.js файл. index.html должен выглядеть примерно так:

Обратите внимание, что при загрузке страницы jQuery $ будет доступен глобально для вызова на странице. Еще одно место для ссылки на знак доллара - это непосредственно объект window, например этот window.$ или этот window['$'].

Мы будем использовать только [<Emit>] для записи привязки. Предположим, мы хотим сделать эту привязку функциональной. Это выглядит следующим образом: определите тип экземпляра jQuery, это будет пустой тип, чтобы указать, когда функция возвращает элемент jQuery или что-то еще:

Создание интерфейса этого типа гарантирует, что тип не генерирует никакого дополнительного кода, он существует только для средства проверки типов. Теперь мы определяем наш JQuery модуль. Сначала вы думаете о том, как вы хотите использовать привязку. Я хочу иметь возможность использовать его в функциональном стиле так же, как я использую Seq, Async или List и т. Д.:

Я хочу, чтобы он сгенерировал что-то вроде следующего с цепочкой:

const div = window['$']("#main")
div.css("background-color", "red")
   .click(function(ev) { console.log("Clicked")})
   .addClass("fancy-class");

Обратите внимание, я буду использовать JQuery.select как псевдоним $. Мне понадобится ссылка на этот знак доллара, но я не могу использовать его просто как [<Emit("$(...)")>], потому что знак доллара зарезервирован для выражений emit. Однако внутри браузера каждая глобально доступная переменная - это просто свойство объекта window. Так что я могу получить ссылку на $ из window вот так:

Обратите внимание, что при помещении $ в кавычки оно становится разрешенным выражением вывода.

Другие методы определяются аналогично тому, как мы видели раньше:

И так далее и тому подобное для остальных функций, если вы хотите поддерживать весь JQuery. Обратите внимание, что для включения цепочки я передаю el в качестве последнего параметра типа IJQuery и возвращаю IJQuery (большинство функций JQuery возвращают объект JQuery, см. Docs). Это создает хороший функциональный API, хотя это только мое личное предпочтение.

Создание цепочки методов на основе экземпляров для привязки JQuery

Написание привязки jQuery в виде модуля - это всего лишь один из способов включить цепочку методов jquery. Бьюсь об заклад, вы ожидали, что обычный способ объединения методов - это «расставить точки» по api:

Для этого необходимо, чтобы методы были размещены в типе экземпляра, а не в модуле. Раньше мы использовали интерфейс IJQuery, но он был пуст, на этот раз мы заполним этот интерфейс абстрактными методами:

Наблюдения:

  • используя abstract методы только для определения сигнатур их типов.
  • абстрактные методы без атрибута emit компилируются с использованием имени метода.
  • для функций с пользовательскими именами, таких как onClick, я использую [<Emit>] , чтобы вернуться к фактическому имени, то есть click. Сам экземпляр будет первым параметром, поэтому я использую $0.click($1), где $0 - экземпляр, а $1 - аргумент.
  • css имеет параметр в виде кортежа для правильной компиляции в javascript. Если бы я использовал такие параметры, как css : string -> string -> IJQuery, я не смог бы «расставить точки» в коде, и мне пришлось бы использовать css с его параметрами в круглых скобках.
  • Я продолжал использовать JQuery.select для запуска «цепочки».

Для использования так:

Изготовим:

Если вам не нравится придавать всем тип, вы можете быстро перейти к возможностям динамического программирования Fable, хотя я лично не рекомендую использовать эту модель, потому что одной из основных причин выбора F # для компиляции в javascript является мощный тип -system, и если бы я хотел писать динамический код, я бы вообще не стал использовать Fable. В любом случае, каждый в своем роде, вам может понравиться эта модель, вот она:

Работа с объектными литералами

JQuery среди почти всех других библиотек javascript работает с объектными литералами. Они используются в качестве параметров большую часть времени и используются повсеместно.

Используя Fable. Мы хотим иметь возможность создавать объектные литералы и управлять ими безопасным для типов способом. Есть несколько способов сделать это. Например, предположим, что у меня есть воображаемая функция в javascript addTime, которая изначально используется следующим образом:

Как видите, литерал объекта состоит из трех свойств: current имеет тип Date (в javascript). amount имеет тип number, а unit - string. Чтобы представить эти типы в F #, мы будем использовать DateTime для current, int для amount и string для unit. Мы будем использовать тип для представления всего литерала объекта, как показано ниже, назовем его AddTimeProps:

Это выведет это:

На выходе получается простой литерал объекта, чего и ожидает внешняя функция addTime. Обратите внимание: поскольку AddTimeProps не имеет конструкторов, я использовал функцию createEmpty<T>. Это специальная функция Fable, которая создаст пустой литерал объекта, но с заданным параметром типа T. В данном случае T равно AddTimeProps. Также обратите внимание, что мы не используем [<Emit>] со свойствами. Это потому, что они абстрактны, и Fable будет использовать имя предоставленного свойства. Чтобы использовать собственные имена, вы можете использовать атрибут [<Emit>], но с забавным синтаксисом выражения emit, например:

Здесь я заменяю свойство amount на specialAmount, используя [<Emit>], затем он создает свойство с настраиваемым именем, он использует синтаксис «необязательный параметр», чтобы определить, следует ли использовать установщик или получатель для свойства.

Типы строковых литералов, только лучше

Возможно, вам понравится это решение для обеспечения безопасности типов, но на самом деле вы можете добиться большего! Предположим, вы читаете документацию addTime и сталкиваетесь с информацией о том, что свойство unit может иметь только строковые значения «дни», «месяцы» или «годы». Чтобы гарантировать, что никто не забудет эти значения или не запишет их неправильно, мы хотим, чтобы компилятор проверял правильность нашего кода. В этом случае мы можем использовать атрибут [<StringEnum>]. Это похоже на «набор текста» в машинописном тексте. Вы можете определить размеченное объединение с кейсами, у которых нет параметров, и скомпилировать их в строки во время компиляции. Вот пример:

Мы можем использовать это для улучшения типа AddTimeProp еще большей безопасностью типов, изменив тип unit с string на TimeUnit:

Случай размеченного союза при компиляции является верблюжьим. Если вам нужно собственное имя для вашего случая объединения, например «YEARS» вместо «year», вы можете использовать атрибут [<CompiledName>], примененный к case:

Результат становится:

Using [<Pojo>] Attribute

Обычные старые объекты javascript или POJO - это просто еще одно название для литералов объектов. Fable предоставляет полезный атрибут [<Pojo>], который применим к типам записей, чтобы заставить их компилироваться в объектные литералы, вот пример:

Это не меняет того факта, что они по-прежнему неизменяемы. Однако вы все равно можете начать с пустого объекта, используя функцию createEmpty<T>:

Примечание автора Fable, Альфонсо Гарсиа-Каро:

Записи Pojo предназначены только для типобезопасного взаимодействия с библиотеками javascript, которым требуется простой объект (например, компоненты React). В этих записях отсутствуют многие функции, такие как методы экземпляра и статические методы, и нет поддержки отражения.

Использование списка размеченного объединения как литерала объекта

Да, это тоже возможно! Используя предыдущий пример Person, вы бы описали его как размеченное объединение:

Person имеет эти свойства Name и Age, но поскольку это тип суммы, вы можете иметь Name или Age, чтобы быть человеком, что не имеет смысла. Для того, чтобы это работало, вам действительно нужен список Person:

Не совсем идиоматично в F #, но хорошо работает (и выглядит красиво) при взаимодействии с внешними библиотеками. Теперь, когда у вас есть список Person, вы можете использовать специальную функцию keyValueList, предоставленную Fable (в Fable.Core.JsInterop), чтобы превратить этот список в литерал объекта:

Этот тип объектных литералов следует использовать: у вас есть много необязательных свойств объекта, и вы хотите их пару, а остальные игнорируете. Это очень хорошо работает, например, для объектов стиля React или для параметров ajax JQuery.

Вы также можете использовать специальные свойства, используя unbox или новый динамический оператор !!:

Стоит отметить, что Fable попытается преобразовать список в литерал объекта во время компиляции, если значение является постоянным, и во время выполнения, если значение еще не было определено во время выполнения.

Создание строковых объектных литералов

Опять же, если вам лень и вы не хотите придавать всему тип, вы можете использовать другую функцию Fable под названием createObj из Fable.Core. Эта функция создает литерал объекта, подобный этому:

createObj хорош тем, что принимает список пар ключ-значение точно так же, как то, что можно ожидать от литерала объекта. Вы также можете легко вкладывать объекты:

Взаимодействие с существующим кодом Javascript

Все наше взаимодействие с javascript до сих пор осуществлялось посредством создания некоторого настраиваемого кода, который будет внедрен во время компиляции с использованием различных атрибутов и функций, предоставляемых Fable. Теперь пришло время взаимодействовать с существующим кодом javascript и фактически вызывать его из F #. Чтобы узнать это, мы напишем вручную какой-нибудь javascript. Сначала создайте файл с именем custom.js, например:

Этот файл будет содержать две функции, которые мы будем вызывать из F #. parseJson и getValue организованы с использованием модулей для совместимости с webpack:

parseJson попытается преобразовать строку в литерал объекта и вернет null в случае неудачи. getValue попытается получить значение из литерала объекта, используя его строковый индекс, и вернет null, если такое свойство не существует (т. Е. Свойство равно undefined).

Поскольку обе функции возвращают либо результат, либо ноль, это дает им право возвращать Option<'a>. Также обратите внимание, что этот файл находится в том же каталоге src, что и файл App.fs. При импорте пользовательского кода вы используете относительные пути.

Чтобы импортировать эти функции, мы будем использовать функцию import изнутри App.fs следующим образом:

Первый аргумент для import - это значение, которое вы хотите импортировать, в данном случае это функция parseJson, а второй аргумент - это где, из которого вы хотите импортировать это значение, в данном случае из файла с именем custom.js в том же каталоге.

Теперь эти функции доступны для использования:

С выходом:

Другой способ импортировать обе функции или любое количество функций из модуля javascript - использовать функцию importAll, но сначала вы должны поместить объявления в один тип и импортировать все функции из модуля javascript как этот тип:

Модуль javascript может экспортировать одно значение:

Я создал файл default.js внутри каталога src с кодом, приведенным выше. Вы можете импортировать его, используя importDefault:

Взаимодействие с Javascript из пакетов npm

Современные библиотеки javascript распространяются через npm, диспетчер пакетов узлов, в виде модулей. Реже встречаются «встроенные и готовые» библиотеки, которые вы просто добавляете на свою страницу с помощью тега скрипта. В наших приложениях Fable мы определенно хотим взаимодействовать с такими библиотеками. В следующем примере я хочу использовать глупую библиотеку под названием left-pad. Я называю это глупым, потому что эта «библиотека» - это единственная функция, которую используют миллионы, вместо того, чтобы… вы знаете, просто писать функцию самостоятельно, когда она вам понадобится.

В любом случае, я перестану разглагольствовать :), поехали. Это то же самое, что и с нашим custom.js файлом, но вместо того, чтобы указывать путь к библиотеке, вы просто указываете на ее имя. Сначала вы хотите установить библиотеку с помощью npm, запустив npm install --save left-pad. Затем этот пакет добавляется как зависимость к вашему package.json файлу, вы должны увидеть запись "left-pad": "^1.1.3" в вашем dependencies. Он установлен в вашем каталоге node-modules, и с помощью магии Webpack вы можете импортировать его из любого места в вашем F #-коде следующим образом:

Работа с перегрузками

Если вы посмотрите на документацию левой панели, функция leftPad должна иметь еще одну перегрузку только с двумя параметрами. Поскольку вы не можете перегрузить обычные функции F # (например, приведенную выше), вы можете написать ту же функцию с другим (но значимым) именем с двумя параметрами:

Если вы хотите перегрузить метод с тем же именем, вы можете использовать статические методы в классе.

А что насчет того, когда у вас есть функция с одним параметром, но этот параметр может быть string, int или DateTime? Затем вы используете «стертые союзы». Эти специальные типы объединения созданы только для перегрузки типов параметров. Чтобы определить функцию с таким параметром:

Если бы у вас было больше типов, вы бы использовали U4<>, U5<> и т. Д. Чтобы использовать такие функции, вы пишете:

Обратите внимание на забавный оператор !^, специально созданный для работы со стертыми типами и проверки, действительно ли предоставленный тип совместим с типом параметра.

Каррированные и незатейливые функции

Функции в F # каррированы по умолчанию, то есть при использовании нескольких аргументов функция становится функцией с одним параметром, которая возвращает другие функции, вот пример:

Эта функция эквивалентна этой каррированной функции:

На заре Fable он компилировал каррированную функцию как есть с замыканиями:

Но недавно, начиная с бета-версии Fable 1.0, компиляция оптимизирована, и функция не спешит:

Что, если вы хотите явно вернуть такую ​​функцию:

тогда вам нужно будет использовать System.Func в подписи в качестве возвращаемого типа:

Заключение

В Fable есть много способов взаимодействия с javascript. Это позволяет вам использовать всю экосистему javascript и многочисленные библиотеки, опубликованные на npm. Надеюсь, вы многому научились из этой статьи, не забудьте нажать на значок сердца внизу и поделиться!