Несколько часов назад я работал над фрагментом кода React Native, который требовал экспорта динамических изображений из файловой системы. Конечно, я начал с документа React Native Image: https://reactnative.dev/docs/image

В React Native есть отличная документация, поэтому, прочитав это, я подумал, что это будет совсем несложно.

В частности, я хотел иметь файл JSON с данными и список изображений в этих данных с информацией о них. Я не знал, сколько изображений или данных заранее.

После просмотра документации React Native я понял, что есть несколько способов включения изображений, и все они говорят сами за себя…. но ни один из них не позволил бы мне читать изображения непосредственно из JSON в локальной файловой системе без дополнительной работы.

Во-первых, есть старое и классическое «require»:

<Image style={{height: 50, width: 50}} source={require('./assets/images/photo1.jpg')} />

Согласно документам React Native, это делает несколько вещей:

  1. При этом используется функция CommonJS «require», которую Metro ищет, чтобы узнать, когда разместить фотографию в нашем пакете. Он настраивает сборщик Metro (также известный как упаковщик или Metro Server) для включения файла photo1.jpg в пакет на основе наличия ключевого слова «require».
  2. Metro конвертирует ресурс (файл jpg) в объект, который может отображаться с помощью компонента ‹Image /›.
  3. Сам Require возвращает объект, который используется в качестве источника изображения.
  4. Объект в этом случае - это целое число, которое Metro преобразует во время загрузки изображения в местоположение.

Хорошо, это было легко.

Хотя это не очень динамично.

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

Было бы неплохо, если бы у нас был список файлов, например заставка и некоторые другие ресурсы, которые мы хотели бы отобразить. Мы определенно не хотим загружать их, пользователь должен иметь возможность использовать приложение после его установки без лишних хлопот.

Мы можем сделать BIT более динамичным, сделав что-то вроде этого:

const DATA = [
{
   text: “man”,
   image: require(‘./assets/images/photo1.jpg’)
},
{
   text: “woman”,
   image: require(‘./assets/images/photo2.jpg’)
}
]

Это хранит информацию в массиве. Обратите внимание, что «require» НЕ является текстовой строкой. Это по-прежнему исполняемая строка кода, которая возвращает объект, поэтому свойство image этого JS-объекта является другим JS-объектом.

Не верите мне? Зарегистрируйте вывод свойства изображения:

[Info] 12–24 06:02:42.359 27376 27440 I ReactNativeJS: 1
[Info] 12–24 06:02:45.256 27376 27440 I ReactNativeJS: 2

Это ссылка на актив, и ее можно использовать вместо свойства DATA [0] .image - вы можете установить <Image source={1} /> напрямую.

Теперь, возвращаясь к нашему приложению, мы добавили следующий код:

Итак, что здесь произошло?

  1. Строки 1–10, мы создали массив DATA для хранения наших изображений и текста.
  2. Строка 13 - мы использовали механизм React State для сохранения состояния текущего imageVar. Это возвращает массив с двумя переменными - первая - это imageVar, текущее изображение. Мы инициализируем эту переменную с помощью функции useState (0), чтобы установить состояние в 0. Это специальная функция, называемая Hook, которая позволяет подключаться к React Feature, добавляя к ней состояние. Во-вторых, он возвращает setImageVar, функцию, которая используется для доступа к состоянию и изменения значения. Это эквивалентно setattr.
  3. Затем в строке 19 мы настраиваем компонент Image и назначаем исходным источником объект в DATA [imageVar] .image. Здесь значение imageVar по умолчанию равно 0. Свойство изображения - это объект, возвращаемый оператором "require" - в данном случае int в индекс ресурсов.
  4. Строка 20 делает то же самое с текстом.
  5. Строка 21 устанавливает компонент Button - эта кнопка использует функцию setImageVar, возвращенную ранее, для настройки значения. (явно, если imageVar совпадает с размером нашего массива, мы сбрасываем на 0, в противном случае мы добавляем 1)

Волшебство здесь в том, что React Native повторно визуализирует только те компоненты, которые были изменены для повышения производительности. В этом случае использование React State запускает повторную визуализацию только для компонента, состояние которого было изменено - здесь кнопки Image и Text. Думайте об этом как о гигантском обратном вызове, который говорит: «Я регистрируюсь как использующий эту часть состояния. Когда он изменится, меня нужно будет уведомить ».

Опять же, вам все равно нужно указать каждое изображение, которое вы загружаете в этот массив, как обязательное, и где-то включить этот массив. Ничего не выбирается автоматически, потому что оно находится в определенном каталоге, но нет никаких шагов по сборке, о которых нужно беспокоиться, любое изображение, которое вам нужно в коде, автоматически включается в ваш пакет. Если вам нужно изменить изображения, просто измените список требуемых кодов, и новые изображения будут получены компонентом Image без отслеживания 10 разных мест. Это отлично подходит для обслуживания кода.

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

Внимательно изучив наш массив, мы видим, что свойство «image» по-прежнему является объектом, то есть Babel все еще может преобразовать этот вызов «require» в объект JavaScript, гарантируя, что изображение загружено в статические ресурсы.

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

const DATA = ‘{ “image1”: { “text”: “man”, “image”: “./assets/images/photo1.jpg” }, “image2”: { “text”:”woman”, “image”:”./assets/images/photo2.jpg” }}’;

Это беспорядочный способ сказать…. у нас есть набор пар имя \ значение.

“image1”: {
 “text”: “man”,
 “photo”: “assets/images/photo2.jpg”
}

Обратите внимание, что мы убрали требования. Требования нам не помогут - JSON - это пары имя \ значение, поэтому для Metro Packager он выглядит как просто строка. Это означает, что у нас нет нашего файла в системе - он больше не упаковывается.

Нам нужно будет найти способ разместить этот файл в системе.

А пока давайте продолжим и разберем JSON:

let parsedData = JSON.parse(DATA);

И готово! Теперь мы можем просто сделать что-то вроде этого:

console.log(parsedData.image1.text);

И получите такой ответ:

[Info] 12-24 09:31:11.988 21250 22480 I ReactNativeJS: man

Альт!

Изменение нашего компонента изображения на:

<Image style={{height: 50, width: 50}} source={parsedData[imageVar].image}/>

Приводит к этой ошибке:

[Info] 12-24 09:35:19.832 21250 22480 E ReactNativeJS: TypeError: undefined is not an object (evaluating 'parsedData[imageVar].image')

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

Чтобы использовать строку, нам нужно использовать так называемый URI или «Универсальный идентификатор ресурса». На самом деле вы используете это все время - в вашем веб-браузере это то, что вы используете, когда переходите по адресу «http: //». Он сообщает браузеру, что это тип «http». Вы можете попробовать «file: //» для локального файла.

React Native поддерживает то же самое.

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

Давайте начнем со старого почтенного response-native-fs, который был подходящим способом доступа к файловым системам с response-native:

npm ERR! Could not resolve dependency:
npm ERR! peer react-native@"^0.59.5" from [email protected]
npm ERR! node_modules/react-native-fs
npm ERR!   react-native-fs@"*" from the root project

Ой. Что ж, похоже, мы больше не можем использовать react-native-fs с последней версией react. Или, возможно, он нуждается в обновлении. На страницах с проблемами это сложно понять.

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

Для этого обратимся к модулю Expo Filesystem. Кроме того, чтобы установить его в проект React-Native, вам нужно использовать модуль unibrow ... Я имею в виду unimodules. Https://docs.expo.io/versions/latest/sdk/filesystem/#supported-uri-schemes-1

Выглядит устрашающе, но довольно просто. Как только вы это сделаете, ваш проект может не работать.

Мне пришлось загрузить студию Android, заново связать проект с ней, затем перезапустить упаковщик и мой эмулятор Android, что вполне соответствует тому, что происходит, когда вы добавляете новый пакет.

Модуль файловой системы дает нам доступ к вызову - readDirectoryAsync ().

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

Эту простую кнопку можно встроить где-нибудь в код - и она вернет следующий результат:

[Info] 12–28 05:44:41.645 7127 10775 I ReactNativeJS: Reading :file:///data/user/0/com.uritest/files/
[Info] 12–28 05:44:41.650 7127 10775 I ReactNativeJS: File: sonar
12–28 05:44:41.650 7127 10775 I ReactNativeJS: File: ReactNativeDevBundle.js

Хм. Не то, что мы ожидали.

Я ожидал, что там будет больше ресурсов - я имею в виду, что в нашем коде есть следующая строка:

const foo = require(‘./assets/images/photo1.jpg’);

Ну да ладно, давай покопаемся еще немного. Откуда React Native получает ресурсы?

Библиотека изображений React Native содержит следующий вызов: Image.resolveAssetSource ().

Этот вызов позволяет нам видеть URI любого статического ресурса (в данном случае изображения) в нашем сборщике.

Давайте попробуем:

const foo = require(‘./assets/images/photo1.jpg’);
const fooURI = Image.resolveAssetSource(foo).uri;
console.log(“FooURI: “ + fooURI);

Распечатывает:

[Info] 12–28 05:54:20.289 7127 10775 I ReactNativeJS: FooURI: http://10.0.2.2:8081/assets/assets/images/photo1.jpg?platform=android&hash=25052dcebf6333bef4fa5c380130eccd

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

С учетом сказанного, давайте загрузим ресурс локально, чтобы мы могли загрузить его с помощью URI.

Приведенный выше код выполняет несколько действий:

  1. Использует вызов Image.resolveAssetSource () для получения URI этого конкретного актива.
  2. Загружает файл на локальный ресурс - вызов загрузки принимает URI и URI локального файла.
  3. Использует переданный в {uri} возвращаемый параметр функции downloadAsync для печати, куда был загружен файл. Он должен соответствовать переданному нами URI.
  4. Читает каталог, чтобы узнать, получили ли мы файл.
[Info] 12–28 06:24:46.329 7127 10775 I ReactNativeJS: Using :file:///data/user/0/com.uritest/files/
[Info] 12–28 06:24:46.355 7127 10775 I ReactNativeJS: Finished downloading to file:///data/user/0/com.uritest/files/photo1.jpg
[Info] 12–28 06:24:46.358 7127 10775 I ReactNativeJS: File: sonar
12–28 06:24:46.358 7127 10775 I ReactNativeJS: File: ReactNativeDevBundle.js
12–28 06:24:46.358 7127 10775 I ReactNativeJS: File: photo1.jpg

И, как видим, сработало!

Каталог в конце нашей функции показывает, что «photo1.jpg» явно доступен здесь.

Примечание. В указанном URI локального файла уже должны быть созданы все каталоги - то есть, если вы укажете DocDir + ”/ dir1 / dir2 / a.jpg”, то “dir1 / dir2” уже должно существовать.

Затем загрузим это изображение.

Для этого мы будем использовать следующий код:

Давайте выделим здесь некоторые из основных моментов.

Игнорировать строки 3–6: они получают URI локально. Мы могли легко загрузить любой URI откуда угодно. Фактически, при работе с http мы можем просто загрузить изображение напрямую из этого URI. Я использую это только как простой способ получить файл на устройстве для этого шага.

Во-первых, мы не хотим отображать компонент изображения, когда изображение не загружено. Для этого мы:

  1. В строке 8 установите новый обработчик состояния, который дает нам возможность узнать, загрузили ли мы изображение. Мы инициализируем его значением 0 (False).
  2. В строке 12 у нас есть dlImage - объект, который назначается условно, в зависимости от состояния, либо некоторому тексту-заполнителю, либо фактическому изображению.
  3. В строке 52 мы включаем JavaScript для настройки ссылки на компонент для dlImage, который будет отображаться в зависимости от состояния imageDownloaded. Это позволяет нам условно визуализировать изображение.
  4. В строке 30 после загрузки изображения мы устанавливаем состояние imageDownloaded на 1.
  5. Когда пользователь нажимает кнопку, выполняется повторная визуализация компонента приложения (зарегистрированного с использованием этой конкретной переменной состояния), и на этот раз включается только что загруженное изображение.

После всего этого у нас есть возможность загружать файлы локально и загружать изображения (или другие ресурсы) с использованием URI.

Строка 10 устанавливает URI изображения как локальный файл.

Строка 13 загружает этот локальный файл. Магия - это «file: //», который является индикатором URI локальной файловой системы. Затем, после этого, нам нужно указать существующий путь, к которому приложение имеет доступ.

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

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

Создайте 2 каталога над корнем, ресурсами и пакетом React Native.

Затем запустите следующую команду из корня React Native, заменив свои каталоги на d: \ gitrepos \ assets и d: \ gitrepos \ bundle. Это то, что в конечном итоге будет включено в сборку развертывания.

PS D:\gitrepos\uritest> npx react-native bundle — assets-dest d:\gitrepos\assets — entry-file d:\gitrepos\uritest\index.js — bundle-output d:\gitrepos\bundle\out.bundle — verbose
 Welcome to React Native!
 Learn once, write anywhere
info Writing bundle output to:, d:\gitrepos\bundle\out.bundle
info Done writing bundle output
info Copying 8 asset files
info Done copying assets

Если вы не укажете в своем проекте вызов «require», мы не увидим каталога assets \ images в каталоге d: \ gitrepos \ assets. (Или любой другой каталог, который вы указали для параметра assets-dest.

Убедитесь, что вы добавили в вызов, чтобы установить требование:

const foo = require('./assets/images/photo1.jpg');

И вдруг там появляется файл ./assets/images/photo1.jpg!

Хотя это не единственный способ проверить, это, безусловно, один из способов! (Например, мы могли бы распаковать комплект Android, перейдя по адресу localhost: 8081. Мне это показалось проще.)

Прямо сейчас на этом этапе мы сделали следующее:

  • Показано, что мы можем загружать изображения локально из файловой системы.
  • Показано, что мы не можем загружать изображения, которых нет в нашем пакете.
  • Показано, что команда «require» не работает с JSON.
  • Показано, что команда «require» возвращает объект.

Это оставляет нас с основным вопросом: как передать наши активы на устройство, чтобы мы могли их использовать - указав их в файле JSON, который не интерпретирует требования JSON, - чтобы они существовали локально, и пользователь мог использовать наше приложение сразу?

Есть несколько способов решить эту проблему, поэтому давайте рассмотрим каждый из них, а также некоторые из их преимуществ и недостатков.

Мы * могли * указать их как активы в Android и iOS, используя файлы Gradle для их передачи. Это будет рассматривать их как активы, специфичные для ОС, и они должны будут соответствовать правилам для каждой платформы, включая размер, название и характеристики.

Это можно автоматизировать с помощью скрипта сборки Gradle, что приятно. То есть - возьмите их из этого каталога и переместите в эти конкретные места. Это по-прежнему дает нам возможность иметь единый источник Истины для наших изображений, но название будет меняться по ходу, поэтому обратное сопоставление актива с источником, из которого он был получен, будет сложнее.

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

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

Давайте попробуем использовать "require" динамически:

for (var key in parsedData) {
   console.log(parsedData[key].text);
   imgObjects.push(require(parsedData[key].image));
}

Это должно быть довольно очевидно, но вы получите следующую ошибку:

[Thu Dec 24 2020 10:44:55.200]  ERROR    TypeError: undefined is not an object (evaluating 'parsedData[imageVar].image')

Require - это функция времени компиляции, а не функция времени выполнения. То есть он ничего не знает о потоке или выполнении программы, он просто ищет модули, которые необходимо загрузить. (IE - код запускается * после * запуска Packager, а код и ресурсы уже находятся на устройстве Android.)

Это не сработает, нам нужно получить ресурсы на устройстве.

Другой способ обойти это - настроить отдельный файл index.js и сопоставить с помощью словаря «require» ключ - тогда мы можем использовать изображение таким образом. Файл index.js - это код JavaScript, который используется для определения модуля для React Native. Это позволяет вам указать импорт и экспорт, а также другие значения по умолчанию для модуля.

Однако это означает, что изображения должны быть перечислены в 2 разных местах: один раз в JSON, а другой в index.js.

Не идеально. Это не идеально, потому что тогда нам придется вести 2 отдельных листинга - 1 в JSON и 1 в исходных файлах JavaScript. Это также означает, что нам нужен способ перевода из одного списка местоположений в «требуемый» объект или идентификатор индекса.

Мне не нравится хранить одни и те же данные в двух разных местах - это всегда приводит к неудачам, когда я пытаюсь понять, почему изображение не находится в том или ином месте. Мантра опытного разработчика - то есть того, кто потратил недели, пытаясь отследить конкретную ошибку, - заключается в том, что код и ресурсы не должны дублироваться.

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

На самом деле, всегда помните: С достаточным количеством магии сборки можно преодолеть почти все. Это просто вопрос, является ли магия сборки ПРАВИЛЬНЫМ поступком.

Как преимущество, это оставляет нам все наши изображения в простом формате require, который ЛЕГКО использовать с React-native. Это также дает нам красивую текстовую строку для использования в нашем исходном формате.

{
   img1: require('foo.jpg');
}
<Image source=img1 />

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

Затем мы переходим к require.context - функциональности, которая позволяет нам указывать глобус при использовании require. По сути, это позволяет нам указать каталог и потребовать все файлы определенного типа в структуре каталогов. Glob - это подстановочный знак или другой жадный спецификатор, очень похожий на то, что вы использовали бы в командной строке. (Т.е. - требуется * .js)

Обратной стороной является:

  • В настоящее время похоже, что это не работает с Babel, но это изменится в будущем.
  • Вы не знаете, какие ресурсы вы нанесли на карту. Нет красивого массива или словаря компонентов. Итак, вы могли бы сопоставить их, но без косвенного использования их становится намного сложнее. Затем возникают проблемы с чистотой кода. Это хорошо работает с модулями, но не так хорошо с активами, так как к активам нужно ссылаться позже для загрузки, а модули можно использовать по имени.

В остальном это точно так же, как «require» - статический, во время сборки приложения.

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

Один из способов обойти это ограничение - загрузить весь пакет \ архив изображений. Это может быть легко выполнено с помощью CDN или другого механизма. (Здесь вам, конечно, понадобится управление версиями вашего архива, чтобы вы могли проверить наличие последних обновлений.)

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

После загрузки пакет будет распакован, а затем использован, как указано выше - загрузка изображения с использованием индикатора URI «file: //».

У этого есть преимущества в том, что он динамичен, если это необходимо. Изображения можно обновлять «на лету» без необходимости повторно публиковать приложение и заставлять пользователей обновляться.

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

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

Одним из способов достижения этого было бы использование нового модуля Expo «Assets». Это определит ваш архив как актив, а затем вы сможете распаковать этот архив локально. Все файлы будут доступны при запуске вашего приложения без необходимости загрузки. Ваше приложение может предоставлять дополнительную функцию «Проверить наличие обновлений» для получения новых ресурсов.

Это объединяет загружаемые ресурсы с методологией обновления статических ресурсов.

И на этом, конечно же, завершается эта довольно длинная статья, которая должна была быть довольно короткой. :)

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

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

ССЫЛКИ

Код:

Метро :

JavaScript:

Изображение:

Файловая система:

React Native: