Рубрики
Без рубрики

Python в реакции с pyodide

Pyodide позволяет запустить код Python в браузере через Webassembly (WASM). Это отличный вариант … Теги от реагирования, Python, Windassembly.

Pyodide позволяет запустить код Python в браузере через Webassembly (WASM). Это отличный вариант, если я, как и я, вы тот, кто хочет избежать некоторых ограничений работы с JavaScript.

Внимание и запуск включает в себя несколько шагов, описанных в Документы Pyodide :

  1. Включают Pyodide.
  2. Настройте среду Python (загрузите модуль Pyodide WASM и инициализируйте его).
  3. Запустите свой код Python.

Cool, но было бы приятно обрабатывать все это в многоразовом реагированным компоненте. Как мы можем сделать это работать?

Давайте сделаем это шаг за шагом.

Шаг 1: включить Pyodide

Первая задача достаточно проста: добавить Сценарий тег к документу голова с URL-адресом Pyodide CDN как SRC атрибут. Еще лучше, если вы используете фреймворк, как GATSBY или Next.js (я использовал последний для этого примера), оберните Сценарий внутри Встроенный Голова Компонент, который будет добавлять теги на голова из страницы для вас ( React-Helmet – еще один отличный вариант). Таким образом, вам не придется беспокоиться о случайном порядке, чтобы включить Pyodide в вашем проекте, поскольку он уже является частью вашего компонента.

Давайте назовем наш компонент Pyodide Отказ Вот что мы имеем до сих пор:

import Head from 'next/head'

export default function Pyodide() {
  return (
    
      
    
  )
}

Шаг 2: Настройка среды Python

Здесь вещи становятся сложными.

Наш скрипт прикрепит функцию под названием Лэппиодид к Глобальный объект нашей среды. В браузере это окно объект, но в целом это называется Globalthis Отказ Пока наш скрипт загружен, мы можем назвать эту функцию следующим образом, где indexurl Является ли строка, соответствующая первой части URL CDN с ранее:

globalThis.loadPyodide({
  indexURL: 'https://cdn.jsdelivr.net/pyodide/dev/full/'
})

Возвращаемое значение Лэппиодид Является ли сам модуль Pyodide, который мы в конечном итоге позвоните, чтобы запустить наш код Python. Можем ли мы просто назначить результат к переменной? Не совсем! Нам нужно рассмотреть пару оговорки.

Во-первых, Лэппиодид Требуется некоторое время, чтобы выполнить (к сожалению, несколько секунд), поэтому нам нужно будет назвать его асинхронно. Мы можем справиться с этим с Async/await Отказ Во-вторых, эта функция создает побочные эффекты. Нам понадобится Regive Useffect крючок, который размещен перед вернуть Заявление функционального компонента.

Эффект будет выглядеть что-то подобное:

useEffect(() => {
  ;(async function () {
    pyodide = await globalThis.loadPyodide({
      indexURL: 'https://cdn.jsdelivr.net/pyodide/dev/full/'
    })
  })()
}, [pyodide])

ждать выражение обернуто внутри async IIFE (сразу обращается с выражением функции) это работает как можно скорее.

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

До сих пор наш список зависимостей включает только Pyodide Переменная мы используем для хранения результата Лэппиодид Отказ Тем не менее, вы могли бы заметить, что Pyodide на самом деле еще не определен. Как оказывается, мы не можем просто добавить Пусть pyodide. Выше наше эффект, поскольку это приведет к тому, что значение будет потеряно на каждом визуализации. Нам нужна ценность Pyodide сохраняться через оказывательство.

Для этого мы можем использовать другой крюк, называемый УСЭРЕФ , что хранит нашу муюсимую ценность в . каравники Свойство простого объекта, как так:

import { useEffect, useRef } from 'react'

export default function Pyodide() {
  const pyodide = useRef(null)

  useEffect(() => {
    ;(async function () {
      pyodide.current = await globalThis.loadPyodide({
        indexURL: 'https://cdn.jsdelivr.net/pyodide/dev/full/'
      })
    })()
  }, [pyodide])

  // ...
}

Аргумент, который мы передаем в УСЭРЕФ Устанавливает начальное значение Pyodide.Current к null Отказ Обратите внимание, что Pyodide Сам объект неизменен: он никогда не меняется, даже когда мы обновляем значение его . каравники имущество. В результате наш эффект вызывается только один раз на компонентной горе, который именно то, что мы хотим.

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

Шаг 3: Оцените код Python

Давайте прыгнем прямо в этот.

Мы будем использовать функцию, предоставляемую pyodide под названием Runpython Оценить строку кода Python. Для простоты мы добавим все к новому эффекту:

const [isPyodideLoading, setIsPyodideLoading] = useState(true)
const [pyodideOutput, setPyodideOutput] = useState(null)

useEffect(() => {
  if (!isPyodideLoading) {
    ;(async function () {
      setPyodideOutput(await pyodide.current.runPython(pythonCode))
    })()
  }
}, [isPyodideLoading, pyodide, pythonCode])

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

Здесь мы устанавливаем начальное состояние ISPYODIDELOADING к правда и добавить состояние внутри эффекта, чтобы позвонить runpython. Только когда Pyodide выполняется загрузка. Так же, как с первым эффектом, мы обернуем runpython. внутри async IIFE до Ждите результат. Этот результат затем передан setpyodideoutput , который обновляет переменную pyodideoutput из его первоначального значения null Отказ

Этот эффект имеет три зависимости. Как и раньше, Pyodide Остается постоянным, и поэтому никогда не приведет к воздействию нашего эффекта в RERUN. Мы также ожидаем стоимость Код Python оставаться без изменений, если мы не решили включить какой-то вход пользователей. Независимо от того, мы еще предстоит представить эту переменную. Где мы должны это сделать?

Наша строка Pythoncode действительно является определяющим характеристикой компонента. Таким образом, имеет смысл включить Pythoncode в реквизит . Использование компонента тогда выглядит что-то подобное:


Нам нужно рассмотреть ISPYODIDELOADING , слишком. Это зависимость, которую мы хотим обновить: он должен измениться с правда к ложь Как только Pyodide закончится загрузка и готова к оценке кода Python. Это будет повторно отображать компонент, запустить эффект и соответствовать критериям Если заявление, чтобы позвонить Runpython Отказ Для этого нам нужно будет обновить состояние с Setispyodideloading внутри нашего первого эффекта.

Конечно, нам также нужно сделать результаты!

Полная реакция

Давайте поставим все это вместе как полный рабочий компонент:

import { useEffect, useRef, useState } from 'react'
import Head from 'next/head'

export default function Pyodide({
  pythonCode,
  loadingMessage = 'loading...',
  evaluatingMessage = 'evaluating...'
}) {
  const indexURL = 'https://cdn.jsdelivr.net/pyodide/dev/full/'
  const pyodide = useRef(null)
  const [isPyodideLoading, setIsPyodideLoading] = useState(true)
  const [pyodideOutput, setPyodideOutput] = useState(evaluatingMessage)

  // load pyodide wasm module and initialize it
  useEffect(() => {
    ;(async function () {
      pyodide.current = await globalThis.loadPyodide({ indexURL })
      setIsPyodideLoading(false)
    })()
  }, [pyodide])

  // evaluate python code with pyodide and set output
  useEffect(() => {
    if (!isPyodideLoading) {
      const evaluatePython = async (pyodide, pythonCode) => {
        try {
          return await pyodide.runPython(pythonCode)
        } catch (error) {
          console.error(error)
          return 'Error evaluating Python code. See console for details.'
        }
      }
      ;(async function () {
        setPyodideOutput(await evaluatePython(pyodide.current, pythonCode))
      })()
    }
  }, [isPyodideLoading, pyodide, pythonCode])

  return (
    <>
      
        
      
      
Pyodide Output: {isPyodideLoading ? loadingMessage : pyodideOutput}
) }

Как обещали, у нас сейчас есть Pythoncode включен как один из компонентов реквизит . Мы также добавили Setispyodideloading к первому эффекту, вызывая его внутри async Функция после Лэппиодид решает. Кроме того, мы видим pyodideoutput внутри Div , который обернут внутри Реагистрационный фрагмент под Голова компонент. Есть несколько других дополнений в код, а также. Давайте пойдем над ними.

Наша вывод оказывается условно. Первоначально ISPYODIDELOADING это истинный Так что LoadeMessage отображается. Когда ISPYODIDELOADING становится ложь , pyodideoutput отображается вместо этого. Однако, хотя Pyodide закончил загрузку в этот момент, что не означает Runpython закончен оценивать код. Нам нужен ОценкаМессаж в это время.

Во многих случаях это сообщение появится только для доли секунды, но для более сложного кода это может повесить на гораздо дольше. Чтобы сделать это работать, мы настроили ОценкаМессаж как начальное значение pyodideoutput Отказ Реагистрационный компонент повторно оказывает в любое время его изменения Таким образом, мы можем быть уверены, что все наши выходы отображаются, как ожидалось. Оба сообщения были добавлены к реквизит с строковым значением по умолчанию.

Мы также инкапсулировали немного содержания второго эффекта внутри асинхронной функции, называемой Оценитьптон , который добавляет попробуй ... поймать Заявление для обработки любых ошибок, которые могут возникнуть при вызове Runpython Отказ

Наконец, мы добавили переменную под названием indexurl. Так что это может быть легко обновлено, если это необходимо. Его значение передается Лэппиодид и встроен в Шаблон литерал построить полный SRC строка скрипт тег.

Здорово! У нас работающий компонент Pyodide. Вот и все, верно?!? Ну нет… К сожалению, у нас есть одна окончательная проблема для решения.

Одна окончательная проблема: несколько компонентов

Если все, что вы хотите, это единственный компонент Pyodide на вашей странице, вы хотите пойти. Однако, если вы заинтересованы в нескольких компонентах на странице, попробуйте. Вы получите ошибку:

Uncaught (in promise) Error: Pyodide is already loading.

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

Реагировать контекст

Введите Реагировать контекст . Этот API позволяет нам делиться глобальными данными между компонентами без необходимости иметь дело с некоторыми внешними государственной библиотекой управления. Он работает через создание объекта контекста, который поставляется со специальным компонентом, называемым поставщиком. Поставщик охватывается вокруг компонента высокого уровня в дереве (обычно корня приложения) и занимает ценность опора передается на дочерние компоненты, которые подписываются на него. В нашем случае мы будем использовать упреждающий текст Крючок, чтобы прослушать изменения в поставщике ценность пропры

Хорошо, поэтому нам нужно создать компонент поставщика. Мы назовем это PyodideProvider Отказ Давайте начнем с идентификации значений, что все наши компоненты нижнего уровня должны понимать.

Компонент поставщика

Наша цель – обеспечить, чтобы только первый компонент Peodide на странице звонит Лэппиодид Таким образом, мы знаем, что нам нужно будет создать некоторое условие в первом эффекте, который зависит от общего значения, описывающего, или нет ?| Лэппиодид был вызван. Давайте будем явным по этому поводу и позвонить в это значение HasloadPyodidebeencalled Отказ Это должно быть логическим, который изначально устанавливается на ложь , а затем изменился на правда Отказ Когда происходит это изменение?

Ну, так как Лэппиодид асинхронно, обновление HasloadPyodidebeencalled должно произойти до того, как позвонить Лэппиодид быть любого использования. Это причина, по которой мы на самом деле нужна новая переменная для нашего условия, а не использовать ISPYODIDELOADING как во втором эффекте. Мы не можем дождаться загрузки Pyodide. Вместо этого информация немедленно должна распространяться на наше значение контекста, чтобы сохранить последующие компоненты, прежде чем они получат обновление.

Это нужно на самом деле приводит нас к другому, более тонким требованию к тому, как мы обращаемся к HasloadPyodidebeencalled Отказ Глобальные значения, которые мы определяем, нужно сохранять через компонент, означающие, что они должны быть установлены с УСЭРЕФ или Уместите Отказ Хотя Уместите может показаться естественным вариантом, оказывается, это не будет работать. Реагирование не гарантирует немедленные обновления государства. Вместо этого, это рассасывает несколько SetState звонит асинхронно. Использование состояния для обработки нашего обновления до HasloadPyodidebeencalled скорее всего, будет слишком медленно, чтобы предотвратить позже компоненты отзывы Лэппиодид больше чем единожды. К счастью, УСЭРЕФ Не страдает от этой задержки: изменения отражены прямо сейчас, поэтому мы будем использовать этот крюк вместо этого.

Есть ли другие ценности, которые необходимо поделиться глобально? Ага! Есть еще три: Pyodide , ISPYODIDELOADING и Setispyodideloading Отказ

С Лэппиодид Сейчас называется только один раз, он также назначен только один раз в Pyodide.Current Модуль WASM мы хотим поделиться между всеми компонентами Pyodide на странице. Кроме того, Setispyodideloading Вызывается внутри состояния первого эффекта, которое снова работает только для первого компонента на странице. Эта функция соединена с переменной состояния ISPYODIDELOADING , значение, которое, когда обновляется, необходимо вызвать второй эффект для каждого компонента. В результате каждая из этих переменных должна использоваться глобально через контекст.

Давайте поставим все это вместе. Вот полный компонент поставщика:

import { createContext, useRef, useState } from 'react'

export const PyodideContext = createContext()

export default function PyodideProvider({ children }) {
  const pyodide = useRef(null)
  const hasLoadPyodideBeenCalled = useRef(false)
  const [isPyodideLoading, setIsPyodideLoading] = useState(true)

  return (
    
      {children}
    
  )
}

Сначала мы создаем и экспортируем контекстный объект под названием PyodideContext Использование CreateContext. . Тогда мы экспортируем наши PyodideProvider. как по умолчанию Оберните PyodideContext. Провайдер вокруг любой дети Это может существовать и пройти наши глобальные переменные в ценность пропры

Компонент поставщика может быть импортирован, где это необходимо в приложении. В Next.js, например, упаковка PyodideProvider. Вокруг корня приложения происходит в _app.js Файл и выглядит что-то вроде этого:

import PyodideProvider from '../components/pyodide-provider'

export default function MyApp({ Component, pageProps }) {
  return (
    
      
    
  )
}

Окончательный компонент Pyodide

Наконец, мы готовы к окончательному Pyodide компоненту, который можно включить несколько раз на одну страницу.

Нам нужно только сделать несколько корректировок к оригинальному компоненту. Для начала нам придется импортировать PyodideContext от нашего провайдера и извлечь глобальные значения от него с помощью упреждающий текст Отказ Затем мы обновляем наш первый эффект, как описано ранее, чтобы включить HasloadPyodidebeencalled Отказ

Наконец, мы добавляем HasloadPyodidebeencalled к списку зависимости первого эффекта, наряду с Setispyodideloading Отказ В том числе последнее необходимо, потому что, хотя реагировать гарантирует, что SetState Функции стабильны и не изменятся на рендеры (поэтому мы можем изначально исключить его), мы теперь получаем стоимость от упреждающий текст Отказ Поскольку этот контекст определен в провайдере, наш отдельный Pyodide Compoter не имеет способов знать что Setispyodideloading действительно стабилен.

Это все это! Вот оно итоговый компонент Pyodide:

import { useContext, useEffect, useState } from 'react'
import Head from 'next/head'
import { PyodideContext } from './pyodide-provider'

export default function Pyodide({
  pythonCode,
  loadingMessage = 'loading...',
  evaluatingMessage = 'evaluating...'
}) {
  const indexURL = 'https://cdn.jsdelivr.net/pyodide/dev/full/'
  const {
    pyodide,
    hasLoadPyodideBeenCalled,
    isPyodideLoading,
    setIsPyodideLoading
  } = useContext(PyodideContext)
  const [pyodideOutput, setPyodideOutput] = useState(evaluatingMessage)

  useEffect(() => {
    if (!hasLoadPyodideBeenCalled.current) {
      hasLoadPyodideBeenCalled.current = true
      ;(async function () {
        pyodide.current = await globalThis.loadPyodide({ indexURL })
        setIsPyodideLoading(false)
      })()
    }
  }, [pyodide, hasLoadPyodideBeenCalled, setIsPyodideLoading])

  useEffect(() => {
    if (!isPyodideLoading) {
      const evaluatePython = async (pyodide, pythonCode) => {
        try {
          return await pyodide.runPython(pythonCode)
        } catch (error) {
          console.error(error)
          return 'Error evaluating Python code. See console for details.'
        }
      }
      ;(async function () {
        setPyodideOutput(await evaluatePython(pyodide.current, pythonCode))
      })()
    }
  }, [isPyodideLoading, pyodide, pythonCode])

  return (
    <>
      
        
      
      
Pyodide Output: {isPyodideLoading ? loadingMessage : pyodideOutput}
) }

Я добавил оба Pyodide Реагистрационный компонент и Провайдер к гисту, а также. Не стесняйтесь рассматривать их здесь Отказ

Оригинал: “https://dev.to/lambertbrady/python-in-react-with-pyodide-1mof”