Skip to main content

使用 React Native 随时随地使用表格

React Native 是一个移动应用框架。它构建使用 JavaScript 来描述布局和事件的 iOS 和 Android 应用。

¥React Native is a mobile app framework. It builds iOS and Android apps that use JavaScript for describing layouts and events.

SheetJS 是一个用于从电子表格读取和写入数据的 JavaScript 库。

¥SheetJS is a JavaScript library for reading and writing data from spreadsheets.

该演示使用 React Native 和 SheetJS 来处理和生成电子表格。我们将探讨如何通过几种方式在 React Native 应用中加载 SheetJS:

¥This demo uses React Native and SheetJS to process and generate spreadsheets. We'll explore how to load SheetJS in a React Native app in a few ways:

"本地文件" 示例创建了一个如下屏幕截图所示的应用:

¥The "Local Files" example creates an app that looks like the screenshots below:

iOSAndroid

iOS screenshot

Android screenshot

在测试此演示之前,请遵循官方 React Native CLI 指南![^1]

¥Before testing this demo, follow the official React Native CLI Guide![^1]

请按照 iOS(需要 macOS)和 Android 的说明进行操作。它们将涵盖安装和系统配置。你应该能够在 Android 和 iOS(如果适用)模拟器中构建并运行示例应用。

¥Follow the instructions for iOS (requires macOS) and for Android. They will cover installation and system configuration. You should be able to build and run a sample app in the Android and the iOS (if applicable) simulators.

集成详情

¥Integration Details

SheetJS NodeJS 模块 可以从应用中的任何组件或脚本导入。

¥The SheetJS NodeJS Module can be imported from any component or script in the app.

内部状态

¥Internal State

为简单起见,该演示使用 "数组的数组"[^2] 作为内部状态。

¥For simplicity, this demo uses an "Array of Arrays"[^2] as the internal state.

SpreadsheetArray of Arrays

pres.xlsx data

[
["Name", "Index"],
["Bill Clinton", 42],
["GeorgeW Bush", 43],
["Barack Obama", 44],
["Donald Trump", 45],
["Joseph Biden", 46]
]

结构中的每个数组对应一行。

¥Each array within the structure corresponds to one row.

该演示还以单个数字数组的形式跟踪列宽。显示组件使用宽度。

¥This demo also keeps track of the column widths as a single array of numbers. The widths are used by the display component.

完整状态

¥Complete State

完整的状态使用以下代码片段进行初始化:

¥The complete state is initialized with the following snippet:

const [data, setData] = useState([
"SheetJS".split(""),
[5,4,3,3,7,9,5],
[8,6,7,5,3,0,9]
]);
const [widths, setWidths] = useState(Array.from({length:7}, () => 20));

更新状态

¥Updating State

从 SheetJS 工作表对象开始,带有 header 选项的 sheet_to_json[^3] 可以生成数组的数组:

¥Starting from a SheetJS worksheet object, sheet_to_json[^3] with the header option can generate an array of arrays:

/* assuming `wb` is a SheetJS workbook */
function update_state(wb) {
/* convert first worksheet to AOA */
const wsname = wb.SheetNames[0];
const ws = wb.Sheets[wsname];
const data = utils.sheet_to_json(ws, {header:1});

/* update state */
setData(data);

/* update column widths */
setWidths(make_width(data));
}

计算列宽

¥Calculating Column Widths

可以通过遍历每列并计算最大数据宽度来计算列宽度。使用数组的数组:

¥Column widths can be calculated by walking each column and calculating the max data width. Using the array of arrays:

/* this function takes an array of arrays and generates widths */
function make_width(aoa) {
/* walk each row */
aoa.forEach((r) => {
/* walk each column */
r.forEach((c, C) => {
/* update column width based on the length of the cell contents */
res[C] = Math.max(res[C]||60, String(c).length * 10);
});
});
/* use a default value for columns with no data */
for(let C = 0; C < res.length; ++C) if(!res[C]) res[C] = 60;
return res;
}

导出状态

¥Exporting State

aoa_to_sheet[^4] 从数组的数组构建一个 SheetJS 工作表对象:

¥aoa_to_sheet[^4] builds a SheetJS worksheet object from the array of arrays:

/* generate a SheetJS workbook from the state */
function export_state() {
/* convert AOA back to worksheet */
const ws = utils.aoa_to_sheet(data);

/* build new workbook */
const wb = utils.book_new();
utils.book_append_sheet(wb, ws, "SheetJS");

return wb;
}

显示数据

¥Displaying Data

该演示使用 react-native-table-component 显示第一个工作表。

¥The demos uses react-native-table-component to display the first worksheet.

演示使用类似于以下示例的组件:

¥The demos use components similar to the example below:

import { ScrollView } from 'react-native';
import { Table, Row, Rows, TableWrapper } from 'react-native-table-component';

(
{/* Horizontal scroll */}
<ScrollView horizontal={true} >
{/* Table container */}
<Table>
{/* Frozen Header Row */}
<TableWrapper>
{/* First row */}
<Row data={data[0]} widthArr={widths}/>
</TableWrapper>
{/* Scrollable Data Rows */}
<ScrollView>
<TableWrapper>
{/* Remaining Rows */}
<Rows data={data.slice(1)} widthArr={widths}/>
</TableWrapper>
</ScrollView>
</Table>
</ScrollView>
)

Rows 组件中的 data.slice(1) 返回从第二行开始的数据。这巧妙地跳过了第一个标题行。

¥data.slice(1) in the Rows component returns data starting from the second row. This neatly skips the first header row.

获取远程数据

¥Fetching Remote Data

0.72.0[^5] 开始的 React Native 版本支持 fetch 中的二进制数据。

¥React Native versions starting from 0.72.0[^5] support binary data in fetch.

此代码片段下载并解析 https://xlsx.nodejs.cn/pres.xlsx

¥This snippet downloads and parses https://xlsx.nodejs.cn/pres.xlsx:

/* fetch data into an ArrayBuffer */
const ab = await (await fetch("https://xlsx.nodejs.cn/pres.xlsx")).arrayBuffer();
/* parse data */
const wb = XLSX.read(ab);

获取演示

¥Fetch Demo

测试部署

本 demo 在以下环境下进行了测试:

¥This demo was tested in the following environments:

真实设备

¥Real Devices

OS设备RN日期
iOS 15.1iPhone 12 Pro 最大0.73.62024-03-13
安卓 29英伟达盾0.73.62024-03-13

模拟器

¥Simulators

OS设备RN开发平台日期
安卓 34像素 3a0.73.6darwin-x642024-03-13
iOS 17.4iPhone 15 Pro 最大0.73.6darwin-x642024-03-13
安卓 34像素 3a0.74.2darwin-arm2024-06-20
iOS 17.5iPhone SE(第 3 代)0.74.2darwin-arm2024-06-20
安卓 34像素 3a0.73.5win10-x642024-03-05
安卓 34像素 3a0.73.7linux-x642024-04-29
  1. 安装 React Native 依赖

    ¥Install React Native dependencies

Installation Notes (click to show)

On the Steam Deck, JDK17 was installed using pacman:

sudo pacman -Syu jdk17-openjdk

The Android Studio tarball was downloaded and extracted. After extracting:

cd ./android-studio/bin
./studio.sh

In Android Studio, select "SDK Manager" and switch to the "SDK Tools" tab. Check "Show Package Details" and install "Android SDK Command-line Tools (latest)".

When this demo was last tested, the following environment variables were used:

export ANDROID_HOME=~/Android/Sdk
export PATH=$PATH:$ANDROID_HOME/emulator:$ANDROID_HOME/platform-tools
export JAVA_HOME=/usr/lib/jvm/java-17-openjdk
  1. 创建项目:

    ¥Create project:

npx -y react-native@0.74.2 init SheetJSRNFetch --version="0.74.2"
  1. 安装共享依赖:

    ¥Install shared dependencies:

cd SheetJSRNFetch
curl -LO https://xlsx.nodejs.cn/logo.png
npm i -S https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz
npm i -S react-native-table-component@1.2.2 @types/react-native-table-component
  1. 下载 App.tsx 并替换:

    ¥Download App.tsx and replace:

curl -LO https://xlsx.nodejs.cn/reactnative/App.tsx

安卓测试

¥Android Testing

  1. 安装或切换到 Java 17[^6]

    ¥Install or switch to Java 17[^6]

  2. 启动 Android 模拟器:

    ¥Start the Android emulator:

npx react-native run-android

在 Linux 上,该命令可能会默默地停止。强烈建议启动交互式 CLI 工具:

¥On Linux, the command may silently stall. It is strongly recommended to launch the interactive CLI tool:

npx react-native start

一旦开发服务器准备就绪,终端将显示一些选项。按 a 在 Android 上运行。

¥Once the dev server is ready, the terminal will display a few options. Press a to run on Android.

如果初始启动因引用模拟器错误而失败,请从 Android Studio 手动启动模拟器,然后重试。

¥If the initial launch fails with an error referencing the emulator, manually start the emulator from Android Studio and try again.

Gradle 错误通常源于 Java 版本不匹配:

¥Gradle errors typically stem from a Java version mismatch:

> Failed to apply plugin 'com.android.internal.application'.
> Android Gradle plugin requires Java 17 to run. You are currently using Java 11.

此错误可以通过安装并切换到请求的版本来解决。

¥This error can be resolved by installing and switching to the requested version.

上次在 L​​inux 上测试此演示时,该进程无法启动模拟器:

¥When this demo was last tested on Linux, the process failed to launch the emulator:

warn Please launch an emulator manually or connect a device. Otherwise app may fail to launch.

这是 React Native 中的一个已知错误!

¥This is a known bug in React Native!

如果安装了模拟器,则运行以下命令:

¥If an emulator is installed, run the following command:

npx react-native doctor

Android 下,会出现一个错误:

¥Under Android, there will be one error:

Android Adb - No devices and/or emulators connected. Please create emulator with Android Studio or connect Android device.

f,将显示可用模拟器的列表。选择模拟器(通常是最后一行)并按 Enter。

¥Press f and a list of available emulators will be shown. Select the emulator (typically the last line) and press Enter.

Select the device / emulator you want to use Emulator Pixel_3a_API_34_extension_level_7_x86_64 (disconnected)

绿色文本是虚拟设备的名称(本例中为 Pixel_3a_API_34_extension_level_7_x86_64)。运行以下命令手动启动模拟器:

¥The text in green is the name of the virtual device (Pixel_3a_API_34_extension_level_7_x86_64 in this example). Run the following command to manually start the emulator:

$ANDROID_HOME/tools/emulator -avd Pixel_3a_API_34_extension_level_7_x86_64

(将 Pixel_3a_API_34_extension_level_7_x86_64 替换为虚拟设备的名称)

¥(replace Pixel_3a_API_34_extension_level_7_x86_64 with the name of the virtual device)

要确认 React Native 检测到模拟器,请再次运行以下命令:

¥To confirm React Native detects the emulator, run the following command again:

npx react-native doctor
  1. 打开后,该应用应类似于下面的 "之前" 屏幕截图。点击 "从电子表格导入数据" 后,验证应用是否显示新数据:

    ¥When opened, the app should look like the "Before" screenshot below. After tapping "Import data from a spreadsheet", verify that the app shows new data:

BeforeAfter

before screenshot

after screenshot

iOS 测试

¥iOS Testing

iOS 测试只能在运行 macOS 的 Apple 硬件上执行!

¥iOS testing can only be performed on Apple hardware running macOS!

Xcode 和 iOS 模拟器在 Windows 或 Linux 上不可用。

¥Xcode and iOS simulators are not available on Windows or Linux.

  1. 通过从 ios 子文件夹运行 pod install 来刷新 iOS 项目:

    ¥Refresh iOS project by running pod install from the ios subfolder:

cd ios; pod install; cd -
  1. 启动 iOS 模拟器:

    ¥Start the iOS emulator:

npx react-native run-ios
  1. 打开后,该应用应类似于下面的 "之前" 屏幕截图。点击 "从电子表格导入数据" 后,验证应用是否显示新数据:

    ¥When opened, the app should look like the "Before" screenshot below. After tapping "Import data from a spreadsheet", verify that the app shows new data:

BeforeAfter

before screenshot

after screenshot

Android 设备测试

¥Android Device Testing

  1. 使用 USB 电缆连接 Android 设备。

    ¥Connect an Android device using a USB cable.

如果设备要求允许 USB 调试,请点击 "允许"。

¥If the device asks to allow USB debugging, tap "Allow".

  1. 关闭所有 Android/iOS 模拟器。

    ¥Close any Android / iOS emulators.

  2. 构建 APK 并在设备上运行:

    ¥Build APK and run on device:

npx react-native run-android

iOS 设备测试

¥iOS Device Testing

  1. 使用 USB 电缆连接 iOS 设备。

    ¥Connect an iOS device using a USB cable.

如果设备要求信任计算机,请点击 "相信" 并输入密码。

¥If the device asks to trust the computer, tap "Trust" and enter the passcode.

  1. 关闭所有 Android/iOS 模拟器。

    ¥Close any Android / iOS emulators.

  2. 启用开发者代码签名证书 [^7]。

    ¥Enable developer code signing certificates[^7].

Enabling Code Signing (click to show)

这些说明已针对 Xcode 15.3 进行了验证。

¥These instructions were verified against Xcode 15.3.

A) 在 Xcode 中打开包含的 iOS 工作区:

¥A) Open the included iOS workspace in Xcode:

open ios/SheetJSRNFetch.xcworkspace

B) 在项目导航器中选择 "SheetJSRNFetch":

¥B) Select "SheetJSRNFetch" in the Project Navigator:

Select the project

C) 在主视图中选择 "签名和能力"。

¥C) Select "Signing & Capabilities" in the main view.

D) 在下方栏中选择 "全部",并确保在下拉列表中选择了一个团队:

¥D) Select "All" in the lower bar and ensure a Team is selected in the dropdown:

Xcode Signing and Capabilities

  1. 通过 Homebrew 安装 ios-deploy

    ¥Install ios-deploy through Homebrew:

brew install ios-deploy
  1. 在设备上运行:

    ¥Run on device:

npx react-native run-ios

在某些测试中,由于 "不受信任的开发者",应用无法在设备上运行:

¥In some tests, the app failed to run on the device due to "Untrusted Developer":

Your device management settings do not allow apps from developer ... on this iPhone. You can allow using these apps in Settings.

这些说明已针对 iOS 15.1 进行了验证。

¥These instructions were verified against iOS 15.1.

A) 打开“设置”应用并选择 "一般的" > "VPN 和设备管理"。

¥A) Open the Settings app and select "General" > "VPN & Device Management".

iOS Management

B) 在 "开发者应用" 下选择 Apple 开发证书。

¥B) Select the Apple Development certificate under "Developer App".

iOS Certificate Info

在新屏幕中,名称 "SheetJSRNFetch" 将显示在应用列表中。

¥In the new screen, the name "SheetJSRNFetch" will be displayed in the Apps list.

C) 在弹出窗口中点击 "相信 ..." 并点击 "相信"。

¥C) Tap "Trust ..." and tap "Trust" in the popup.

iOS Trust Popup

D) 关闭设备上的“设置”应用。

¥D) Close the Settings app on the device.

E) 关闭计算机上的 Metro 窗口。

¥E) Close the Metro window on the computer.

F) 再次运行 npx react-native run-ios

¥F) Run npx react-native run-ios again.

在某些测试中,构建失败并出现以下错误:

¥In some tests, the build failed with the following error:

PhaseScriptExecution failed with a nonzero exit code

这是由于 react-native 包中的错误造成的。必须编辑脚本 node_modules/react-native/scripts/react-native-xcode.sh

¥This was due to an error in the react-native package. The script node_modules/react-native/scripts/react-native-xcode.sh must be edited.

在脚本顶部附近,将有一个 set 语句:

¥Near the top of the script, there will be a set statement:

node_modules/react-native/scripts/react-native-xcode.sh
# Print commands before executing them (useful for troubleshooting)
set -x -e
DEST=$CONFIGURATION_BUILD_DIR/$UNLOCALIZED_RESOURCES_FOLDER_PATH

必须删除 -e 参数:

¥The -e argument must be removed:

node_modules/react-native/scripts/react-native-xcode.sh (edit line)
# Print commands before executing them (useful for troubleshooting)
set -x
DEST=$CONFIGURATION_BUILD_DIR/$UNLOCALIZED_RESOURCES_FOLDER_PATH

在一些测试运行中,错误提到了开发团队:

¥In some test runs, the error mentioned a development team:

error: Signing for "SheetJSRNFetch" requires a development team. Select a development team in the Signing & Capabilities editor. (in target 'SheetJSRNFetch' from project 'SheetJSRNFetch')

必须在项目中启用代码签名!

¥Code signing must be enabled in the project!

默认情况下,React Native 生成专门针对 iPhone 的应用。在物理 iPad 上,将运行像素化的 iPhone 应用。

¥By default, React Native generates applications that exclusively target iPhone. On a physical iPad, a pixellated iPhone app will be run.

必须更改 "目标器件系列" 设置才能支持 iPad:

¥The "targeted device families" setting must be changed to support iPad:

A) 打开 Xcode 工作区:

¥A) Open the Xcode workspace:

open ./ios/SheetJSRNFetch.xcworkspace

B) 在左侧栏中选择项目:

¥B) Select the project in the left sidebar:

Select the project

C) 在侧栏中选择 "SheetJSRNFetch" 目标。

¥C) Select the "SheetJSRNFetch" target in the sidebar.

Settings

D) 选择主区域中的 "构建设置" 选项卡。

¥D) Select the "Build Settings" tab in the main area.

E) 在 "构建设置" 下面的搜索栏中,输入 "tar"

¥E) In the search bar below "Build Settings", type "tar"

F) 在 "目标设备系列" 行中,将值更改为 "iPhone、iPad"。

¥F) In the "Targeted Device Families" row, change the value to "iPhone, iPad".

在某些测试运行中,构建失败并显示一条配置消息:

¥In some test runs, the build failed with a Provisioning message:

error: Provisioning profile "..." doesn't include the currently selected device "..." (identifier ...).

通过手动选择目标设备解决了这个问题:

¥This was resolved by manually selecting the target device:

A) 打开 Xcode 工作区:

¥A) Open the Xcode workspace:

open ./ios/SheetJSRNFetch.xcworkspace

B) 在左侧栏中选择项目:

¥B) Select the project in the left sidebar:

Select the project

C) 在顶部栏中,项目名称旁边将有一个灰色的设备图标。单击该图标并从列表中选择真实设备。

¥C) In the top bar, next to the project name, there will be a gray device icon. Click on the icon and select the real device from the list.

本地文件

¥Local Files

React Native 不提供原生文件选择器或从设备上的文档读取和写入数据的方法。必须使用第三方库。

¥React Native does not provide a native file picker or a method for reading and writing data from documents on the devices. A third-party library must be used.

由于 React Native 内部结构在版本之间发生变化,因此库可能仅适用于特定版本的 React Native。在选择库之前应查阅项目文档。

¥Since React Native internals change between releases, libraries may only work with specific versions of React Native. Project documentation should be consulted before picking a library.

下表列出了经过测试的文件插件。"OS" 列出了经过测试的平台("A" 适用于 Android,"I" 适用于 iOS)。

¥The following table lists tested file plugins. "OS" lists tested platforms ("A" for Android and "I" for iOS).

文件系统插件文件选择器插件OS
react-native-file-accessreact-native-document-pickerAI
react-native-blob-utilreact-native-document-pickerAI
expo-file-systemexpo-document-pickerAI

应用配置

¥App Configuration

出于隐私考虑,应用必须请求文件访问权限。有用于访问数据的特殊 API,并且在未来的平台版本中可能会发生变化。

¥Due to privacy concerns, apps must request file access. There are special APIs for accessing data and are subject to change in future platform versions.

Technical Details (click to show)

iOS

iOS applications typically require two special settings in Info.plist:

  • UIFileSharingEnabled[^8] allows users to use files written by the app. A special folder will appear in the "Files" app.

  • LSSupportsOpeningDocumentsInPlace[^9] allows the app to open files without creating a local copy.

Both settings must be set to true:

Info.plist (add to file)
<plist version="1.0">
<dict>
<key>UIFileSharingEnabled</key>
<true/>
<key>LSSupportsOpeningDocumentsInPlace</key>
<true/>
<key>CFBundleDevelopmentRegion</key>

Once the options are set, generated files are visible to users and can be shared with other apps including "Mail", "Messages", and "Numbers".

Android

Permissions and APIs have evolved over time. For broadest compatibility, the following permissions must be enabled in AndroidManifest.xml:

  • READ_EXTERNAL_STORAGE and WRITE_EXTERNAL_STORAGE allow apps to access files outside of the app scope. These are required for scoped storage access.

  • android:requestLegacyExternalStorage="true" enabled legacy behavior in some older releases.

The manifest is saved to android/app/src/main/AndroidManifest.xml:

android/app/src/main/AndroidManifest.xml (add highlighted lines)
    <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<application
android:requestLegacyExternalStorage="true"
android:name=".MainApplication"
android:label="@string/app_name"
android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher_round"
android:allowBackup="false"
android:theme="@style/AppTheme">

Depending on the Android API level, there are three strategies for writing files:

  • In "legacy" mode (supported in API levels up to 29), files can be written to the user Downloads or Documents folder directly.

  • Using the MediaStore API, files should be copied to a visible location.

  • Using the "Storage Access Framework", the user grants access to a folder and the app uses SAF APIs to create files and write data.

RN 文件选择器

¥RN File Picker

"文件选择器" 库处理两个特定于平台的步骤:

¥The "File Picker" library handles two platform-specific steps:

  1. 显示允许用户从其设备中选择文件的视图

    ¥Show a view that allows users to select a file from their device

  2. 将所选文件复制到应用可以读取的位置

    ¥Copy the selected file to a location that can be read by the application

以下库已经过测试:

¥The following libraries have been tested:

react-native-document-picker

react-native-document-picker 提供了一种 pickSingle 方法供用户选择一个文件。

¥react-native-document-picker provides a pickSingle method for users to select one file.

文件插件通常需要 copyTo: "cachesDirectory" 选项:

¥The file plugins generally require the copyTo: "cachesDirectory" option:

import { pickSingle } from 'react-native-document-picker';

const f = await pickSingle({
allowMultiSelection: false,
copyTo: "cachesDirectory",
mode: "open"
});
const path = f.fileCopyUri; // this path can be read by RN file plugins

expo-document-picker

expo-document-picker 是一个与 Expo 生态系统中其他模块配合使用的选择器。

¥expo-document-picker is a picker that works with other modules in the Expo ecosystem.

getDocumentAsync 方法允许用户选择文件。Expo 文件插件需要 copyToCacheDirectory 选项:

¥The getDocumentAsync method allows users to select a file. The Expo file plugin requires the copyToCacheDirectory option:

import * as DocumentPicker from 'expo-document-picker';

const result = await DocumentPicker.getDocumentAsync({
copyToCacheDirectory: true,
type: ['application/vnd.ms-excel', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet']
});
const path = result.assets[0].uri; // this path can be read by RN file plugins

RN 文件插件

¥RN File Plugins

以下库已经过测试:

¥The following libraries have been tested:

react-native-blob-util

react-native-blob-util 是 2016 年其他库的延续。

¥react-native-blob-util is the continuation of other libraries that date back to 2016.

ascii 类型返回与原始字节对应的数字数组。数据中的 Uint8Arraybuffer 类型兼容。

¥The ascii type returns an array of numbers corresponding to the raw bytes. A Uint8Array from the data is compatible with the buffer type.

Reading and Writing snippets (click to hide)

Reading Data

import * as XLSX from "xlsx";
import RNFetchBlob from 'react-native-blob-util';
const { readFile } = RNFetchBlob.fs;

const res = await readFile(path, 'ascii');
const wb = XLSX.read(new Uint8Array(res), {type:'buffer'});

On iOS, the URI from react-native-document-picker must be massaged:

import { pickSingle } from 'react-native-document-picker';
import RNFetchBlob from 'react-native-blob-util';
const { readFile, dirs: { DocumentDir } } = RNFetchBlob.fs;

const f = await pickSingle({
// Instruct the document picker to copy file to Documents directory
copyTo: "documentDirectory",
allowMultiSelection: false, mode: "open" });
// `f.uri` is the original path and `f.fileCopyUri` is the path to the copy
let path = f.fileCopyUri;
// iOS workaround
if (Platform.OS === 'ios') path = path.replace(/^.*\/Documents\//, DDP + "/");

const res = await readFile(path, 'ascii');

Writing Data

import * as XLSX from "xlsx";
import RNFetchBlob from 'react-native-blob-util';
const { writeFile, readFile, dirs:{ DocumentDir } } = RNFetchBlob.fs;

const wbout = XLSX.write(wb, {type:'buffer', bookType:"xlsx"});
const file = DocumentDir + "/sheetjsw.xlsx";
const res = await writeFile(file, Array.from(wbout), 'ascii');

Sharing Files in Android

copyToMediaStore uses the MediaStore API to share files.

The file must be written to the device before using the MediaStore API!

// ... continuation of "writing data"
const { MediaCollection } = RNFetchBlob;

/* Copy to downloads directory (android) */
try {
await MediaCollection.copyToMediaStore({
parentFolder: "",
mimeType: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
name: "sheetjsw.xlsx"
}, "Download", file);
} catch(e) {}

react-native-file-access

react-native-file-access 是一个使用现代 iOS 和 Android 开发模式的文件系统 API。

¥react-native-file-access is a filesystem API that uses modern iOS and Android development patterns.

base64 编码返回与 base64 类型兼容的字符串:

¥The base64 encoding returns strings compatible with the base64 type:

Reading and Writing snippets (click to hide)

Reading Data

import * as XLSX from "xlsx";
import { FileSystem } from "react-native-file-access";

const b64 = await FileSystem.readFile(path, "base64");
/* b64 is a Base64 string */
const workbook = XLSX.read(b64, {type: "base64"});

Writing Data

import * as XLSX from "xlsx";
import { Dirs, FileSystem } from "react-native-file-access";
const DDP = Dirs.DocumentDir + "/";

const b64 = XLSX.write(workbook, {type:'base64', bookType:"xlsx"});
/* b64 is a Base64 string */
await FileSystem.writeFile(DDP + "sheetjs.xlsx", b64, "base64");

Sharing Files in Android

cpExternal uses the MediaStore API to share files.

The file must be written to the device before using the MediaStore API!

// ... continuation of "writing data"

/* Copy to downloads directory (android) */
try {
await FileSystem.cpExternal(file, "sheetjsw.xlsx", "downloads");
} catch(e) {}

expo-file-system

expo-file-system 是一个文件系统 API,可与 Expo 生态系统中的其他模块配合使用。

¥expo-file-system is a filesystem API that works with other modules in the Expo ecosystem.

某些 Expo API 返回无法使用 expo-file-system 读取的 URI。这将表现为一个错误:

¥Some Expo APIs return URI that cannot be read with expo-file-system. This will manifest as an error:

位置 '...' 不受支持的方案

¥Unsupported scheme for location '...'

expo-document-picker 代码片段会生成本地副本。

¥The expo-document-picker snippet makes a local copy.

EncodingType.Base64 编码与 base64 类型兼容。

¥The EncodingType.Base64 encoding is compatible with base64 type.

Reading and Writing snippets (click to hide)

Reading Data

Calling FileSystem.readAsStringAsync with FileSystem.EncodingType.Base64 encoding returns a promise resolving to a string compatible with base64 type:

import * as XLSX from "xlsx";
import * as FileSystem from 'expo-file-system';

const b64 = await FileSystem.readAsStringAsync(uri, { encoding: FileSystem.EncodingType.Base64 });
const workbook = XLSX.read(b64, { type: "base64" });

Writing Data

The FileSystem.EncodingType.Base64 encoding accepts Base64 strings:

import * as XLSX from "xlsx";
import * as FileSystem from 'expo-file-system';

const b64 = XLSX.write(workbook, {type:'base64', bookType:"xlsx"});
/* b64 is a Base64 string */
await FileSystem.writeAsStringAsync(FileSystem.documentDirectory + "sheetjs.xlsx", b64, { encoding: FileSystem.EncodingType.Base64 });

Sharing Files in Android

StorageAccessFramework uses the "Storage Access Framework" to share files.

SAF API methods must be used to request permissions, make files and write data:

import * as XLSX from "xlsx";
import { documentDirectory, StorageAccessFramework } from 'expo-file-system';

const b64 = XLSX.write(workbook, {type:'base64', bookType:"xlsx"});
/* b64 is a Base64 string */
try {
/* request access to a folder */
const perms = await StorageAccessFramework.requestDirectoryPermissionsAsync(documentDirectory);
/* if the user selected a folder ... */
if(perms.granted) {

/* create a new file */
const uri = perms.directoryUri;
const file = await StorageAccessFramework.createFileAsync(uri, "sheetjsw", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");

/* write data to file */
await StorageAccessFramework.writeAsStringAsync(file, wbout, { encoding: "base64" });
}
} catch(e) {}

演示

¥Demo

测试部署

本 demo 在以下环境下进行了测试:

¥This demo was tested in the following environments:

真实设备

¥Real Devices

OS设备RN日期
iOS 15.5iPhone 13 Pro 最大0.73.62024-03-31
安卓 29英伟达盾0.73.62024-03-31

模拟器

¥Simulators

OS设备RN开发平台日期
安卓 34像素 3a0.73.6darwin-x642024-03-31
iOS 17.4iPhone 15 Pro 最大0.73.6darwin-x642024-03-31
安卓 34像素 3a0.73.6win10-x642024-03-31
安卓 34像素 3a0.73.6linux-x642024-03-31

React Native 应用存在许多移动部件和陷阱。强烈建议在进行此演示之前先阅读适用于 iOS 和 Android 的 React Native 官方教程。[^10] 此处不介绍包括 Android 虚拟设备配置在内的详细信息。

¥There are many moving parts and pitfalls with React Native apps. It is strongly recommended to follow the official React Native tutorials for iOS and Android before approaching this demo.[^10] Details including Android Virtual Device configuration are not covered here.

此示例尝试分离特定于库的函数。

¥This example tries to separate the library-specific functions.

项目设置

¥Project Setup

  1. 创建项目:

    ¥Create project:

npx react-native init SheetJSRN --version="0.73.6"

在 macOS 上,如果提示安装 CocoaPods,请按 y

¥On macOS, if prompted to install CocoaPods, press y.

  1. 安装共享依赖:

    ¥Install shared dependencies:

cd SheetJSRN
curl -LO https://xlsx.nodejs.cn/logo.png
npm i -S https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz
npm i -S react-native-table-component@1.2.2 react-native-document-picker@9.1.1
  1. 下载 index.js 并替换:

    ¥Download index.js and replace:

curl -LO https://xlsx.nodejs.cn/mobile/index.js
  1. 启动 Android 模拟器:

    ¥Start the Android emulator:

npx react-native run-android

该应用应类似于以下屏幕截图:

¥The app should look like the following screenshot:

React Native Android App

上次在 Windows 上测试此演示时,构建失败并出现错误:

¥When this demo was last tested on Windows, the build failed with an error:

> Failed to apply plugin 'com.android.internal.application'.
> Android Gradle plugin requires Java 17 to run. You are currently using Java 11.

必须安装 Java 17[^11],并且 JAVA_HOME 环境变量必须指向 Java 17 位置。

¥Java 17 must be installed[^11] and the JAVA_HOME environment variable must point to the Java 17 location.

停止开发服务器并关闭 React Native Metro NodeJS 窗口。

¥Stop the dev server and close the React Native Metro NodeJS window.

文件整合

¥File Integration

  1. 选择一个文件系统库进行集成:

    ¥Pick a filesystem library for integration:

安装 react-native-blob-util 依赖:

¥Install react-native-blob-util dependency:

npm i -S react-native-blob-util@0.19.8

将高亮的行添加到 index.js

¥Add the highlighted lines to index.js:

index.js
import { Table, Row, Rows, TableWrapper } from 'react-native-table-component';

import { read, write } from 'xlsx';
import { pickSingle } from 'react-native-document-picker';
import { Platform } from 'react-native';
import RNFetchBlob from 'react-native-blob-util';

async function pickAndParse() {
const f = await pickSingle({allowMultiSelection: false, copyTo: "documentDirectory", mode: "open" });
let path = f.fileCopyUri;
if (Platform.OS === 'ios') path = path.replace(/^.*\/Documents\//, RNFetchBlob.fs.dirs.DocumentDir + "/");
const res = await (await fetch(path)).arrayBuffer(); // RN >= 0.72
// const res = await RNFetchBlob.fs.readFile(path, 'ascii'); // RN < 0.72
return read(new Uint8Array(res), {type: 'buffer'});
}

async function writeWorkbook(wb) {
const wbout = write(wb, {type:'buffer', bookType:"xlsx"});
const file = RNFetchBlob.fs.dirs.DocumentDir + "/sheetjsw.xlsx";
await RNFetchBlob.fs.writeFile(file, Array.from(wbout), 'ascii');

/* Copy to downloads directory (android) */
try { await RNFetchBlob.MediaCollection.copyToMediaStore({
parentFolder: "",
mimeType: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
name: "sheetjsw.xlsx"
}, "Download", file); } catch(e) {}

return file;
}

const make_width = ws => {

安卓测试

¥Android Testing

  1. 重启 Android 开发进程:

    ¥Restart the Android development process:

npx react-native run-android

将显示以下应用:

¥The following app will be shown:

React Native Android App

上次在 macOS 上测试此演示时,该过程无法启动模拟器:

¥When this demo was last tested on macOS, the process failed to launch the emulator:

warn Please launch an emulator manually or connect a device. Otherwise app may fail to launch.

这是 React Native 中的一个已知错误!

¥This is a known bug in React Native!

如果安装了模拟器,则运行以下命令:

¥If an emulator is installed, run the following command:

npx react-native doctor

Android 下,会出现一个错误:

¥Under Android, there will be one error:

Android Adb - No devices and/or emulators connected. Please create emulator with Android Studio or connect Android device.

f,将显示可用模拟器的列表。选择模拟器(通常是最后一行)并按 Enter。

¥Press f and a list of available emulators will be shown. Select the emulator (typically the last line) and press Enter.

Select the device / emulator you want to use Emulator Pixel_3a_API_34 (disconnected)

绿色文本是虚拟设备的名称(本例中为 Pixel_3a_API_34)。运行以下命令手动启动模拟器:

¥The text in green is the name of the virtual device (Pixel_3a_API_34 in this example). Run the following command to manually start the emulator:

$ANDROID_HOME/tools/emulator -avd Pixel_3a_API_34

(将 Pixel_3a_API_34 替换为虚拟设备的名称)

¥(replace Pixel_3a_API_34 with the name of the virtual device)

要确认 React Native 检测到模拟器,请再次运行以下命令:

¥To confirm React Native detects the emulator, run the following command again:

npx react-native doctor
  1. 下载 https://xlsx.nodejs.cn/pres.numbers 并打开下载文件夹。

    ¥Download https://xlsx.nodejs.cn/pres.numbers and open the Downloads folder.

  2. 单击 pres.numbers 并将其从“下载”文件夹拖到模拟器中。

    ¥Click and drag pres.numbers from the Downloads folder into the simulator.

  3. 单击 "导入数据" 并查找 pres.numbers

    ¥Click "Import data" and look for pres.numbers.

如果未显示该文件,请单击 图标,然后单击 "下载"。

¥If the file is not displayed, click the icon and click "Downloads".

pick file Android

  1. 选择 pres.numbers

    ¥Select pres.numbers.

屏幕应刷新并显示新内容:

¥The screen should refresh with new contents:

read file Android

  1. 单击 "导出数据"。

    ¥Click "Export data".

Android 上的 expo-file-system 将提示授予文件夹访问权限。

¥expo-file-system on Android will prompt to grant access to a folder.

点击 图标,然后点击带有下载图标的 "文件" 文件夹。

¥Tap the icon and tap the "Documents" folder with the download icon.

点击 'ALLOW ACCESS TO "DOCUMENTS"' 按钮。

¥Tap the 'ALLOW ACCESS TO "DOCUMENTS"' button.

在 "允许访问" 弹出窗口中,点击 "ALLOW"。

¥In the "Allow access" pop, tap "ALLOW".

警报将显示文件的位置:

¥An alert will display the location to the file:

write file Android

  1. 从模拟器中提取文件并验证内容:

    ¥Pull the file from the simulator and verify the contents:

adb exec-out run-as com.sheetjsrn cat files/sheetjsw.xlsx > /tmp/sheetjsw.xlsx
npx xlsx-cli /tmp/sheetjsw.xlsx

PowerShell 会破坏重定向中的二进制数据。

¥PowerShell mangles binary data in the redirect.

在 Windows 上,必须在命令提示符中运行以下命令:

¥On Windows, the following commands must be run in the Command Prompt:

adb exec-out run-as com.sheetjsrn cat files/sheetjsw.xlsx > sheetjsw.xlsx
npx xlsx-cli sheetjsw.xlsx
  1. 停止开发服务器并关闭 React Native Metro NodeJS 窗口。

    ¥Stop the dev server and close the React Native Metro NodeJS window.

iOS 测试

¥iOS Testing

iOS 测试只能在运行 macOS 的 Apple 硬件上执行!

¥iOS testing can only be performed on Apple hardware running macOS!

Xcode 和 iOS 模拟器在 Windows 或 Linux 上不可用。

¥Xcode and iOS simulators are not available on Windows or Linux.

向下滚动至 "Android 设备测试" 以进行设备测试。

¥Scroll down to "Android Device Testing" for device tests.

  1. 通过从 ios 子文件夹运行 pod install 来刷新 iOS 项目:

    ¥Refresh iOS project by running pod install from the ios subfolder:

cd ios; pod install; cd -
  1. 开始 iOS 开发流程:

    ¥Start the iOS development process:

npx react-native run-ios
  1. 下载 https://xlsx.nodejs.cn/pres.numbers 并打开下载文件夹。

    ¥Download https://xlsx.nodejs.cn/pres.numbers and open the Downloads folder.

  2. 在模拟器中,单击“主页”图标返回主屏幕。

    ¥In the simulator, click the Home icon to return to the home screen.

  3. 单击 "文件" 图标打开应用。

    ¥Click on the "Files" icon to open the app.

  4. 单击 pres.numbers 并将其从“下载”文件夹拖到模拟器中。

    ¥Click and drag pres.numbers from the Downloads folder into the simulator.

save file iOS

  1. 确保高亮 "在我的 iPhone 上" 并选择 "保存"。

    ¥Make sure "On My iPhone" is highlighted and select "Save".

  2. 再次单击主页图标,然后选择 SheetJSRN 应用。

    ¥Click the Home icon again then select the SheetJSRN app.

  3. 单击 "导入数据" 并选择 pres

    ¥Click "Import data" and select pres:

pick file iOS

选择后,屏幕应刷新并显示新内容:

¥Once selected, the screen should refresh with new contents:

read file iOS

  1. 单击 "导出数据"。

    ¥Click "Export data".

警报将显示文件的位置:

¥An alert will display the location to the file:

write file iOS

  1. 找到该文件并验证内容是否正确:

    ¥Find the file and verify the contents are correct:

find ~/Library/Developer/CoreSimulator -name sheetjsw.xlsx |
while read x; do echo "$x"; npx xlsx-cli "$x"; done

测试完成后,停止模拟器和开发过程。

¥Once testing is complete, stop the simulator and the development process.

Android 设备测试

¥Android Device Testing

  1. 将高亮的行添加到 android/app/src/main/AndroidManifest.xml

    ¥Add the highlighted lines to android/app/src/main/AndroidManifest.xml:

android/app/src/main/AndroidManifest.xml (add highlighted lines)
    <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<application
android:requestLegacyExternalStorage="true"
android:name=".MainApplication"
android:label="@string/app_name"
android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher_round"
android:allowBackup="false"
android:theme="@style/AppTheme">

manifest 标签内将有两个新的 uses-permission 标签。属性 android:requestLegacyExternalStorage="true" 必须添加到 application 标记中。

¥There will be two new uses-permission tags within the parent manifest tag. The attribute android:requestLegacyExternalStorage="true" must be added to the application tag.

  1. 关闭所有 Android / iOS 模拟器。

    ¥Close any Android / iOS simulators.

停止开发服务器并关闭 React Native Metro NodeJS 窗口。

¥Stop the dev server and close the React Native Metro NodeJS window.

  1. 使用 USB 电缆连接 Android 设备。

    ¥Connect an Android device using a USB cable.

如果设备要求允许 USB 调试,请点击 "允许"。

¥If the device asks to allow USB debugging, tap "Allow".

  1. 构建 APK 并在设备上运行:

    ¥Build APK and run on device:

npx react-native run-android
  1. 在设备上下载 https://xlsx.nodejs.cn/pres.numbers

    ¥Download https://xlsx.nodejs.cn/pres.numbers on the device.

  2. 切换回 "SheetJSRN" 应用。

    ¥Switch back to the "SheetJSRN" app.

  3. 点击 "导入数据",然后点击 pres.numbers

    ¥Tap "Import data" and tap pres.numbers.

如果未显示该文件,请点击 图标,然后点击 "下载"。

¥If the file is not displayed, tap the icon and tap "Downloads".

该表将使用文件中的数据刷新。

¥The table will refresh with data from the file.

  1. 点击 "导出数据"。

    ¥Tap "Export Data".

Android 上的 expo-file-system 将提示授予文件夹访问权限。

¥expo-file-system on Android will prompt to grant access to a folder.

点击 图标,然后点击带有下载图标的 "文件" 文件夹。

¥Tap the icon and tap the "Documents" folder with the download icon.

点击 'ALLOW ACCESS TO "DOCUMENTS"' 按钮。

¥Tap the 'ALLOW ACCESS TO "DOCUMENTS"' button.

在 "允许访问" 弹出窗口中,点击 "ALLOW"。

¥In the "Allow access" pop, tap "ALLOW".

exportFile 弹出窗口中点击 "OK"。

¥Tap "OK" in the exportFile popup.

  1. 切换到“文件”应用并导航到“下载”文件夹。

    ¥Switch to the Files app and navigate to the Downloads folder.

测试 expo-file-system 时,选择 "文件"。

¥When testing expo-file-system, select "Documents".

将有一个新文件 sheetjsw.xlsx

¥There will be a new file sheetjsw.xlsx.

  1. 关闭并重新打开 "SheetJSRN" 应用。数据将重置。

    ¥Close and reopen the "SheetJSRN" app. The data will reset.

  2. 点击 "导入数据",然后点击 sheetjsw.xlsx

    ¥Tap "Import data" and tap sheetjsw.xlsx.

如果未显示该文件,请点击 图标,然后点击 "下载"。

¥If the file is not displayed, tap the icon and tap "Downloads".

测试 expo-file-system 时,选择 "文件"。

¥When testing expo-file-system, select "Documents".

该表将使用导出文件中的数据刷新。

¥The table will refresh with the data from the exported file.

iOS 设备测试

¥iOS Device Testing

  1. 关闭所有 Android/iOS 模拟器。

    ¥Close any Android / iOS emulators.

  2. 启用文件共享并使文档文件夹在 iOS 应用中可见。将以下行添加到 ios/SheetJSRN/Info.plist

    ¥Enable file sharing and make the documents folder visible in the iOS app. Add the following lines to ios/SheetJSRN/Info.plist:

ios/SheetJSRN/Info.plist (add to file)
<plist version="1.0">
<dict>
<key>UIFileSharingEnabled</key>
<true/>
<key>LSSupportsOpeningDocumentsInPlace</key>
<true/>
<key>CFBundleDevelopmentRegion</key>

(文档的根元素是 plist,它包含一个 dict 子元素)

¥(The root element of the document is plist and it contains one dict child)

  1. 启用开发者代码签名证书。获取演示 的 "iOS 设备测试" 部分涵盖了更多详细信息(步骤 15)。

    ¥Enable developer code signing certificates. More details are covered in the "iOS Device Testing" part of the Fetch Demo (step 15).

  2. 通过 Homebrew 安装 ios-deploy

    ¥Install ios-deploy through Homebrew:

brew install ios-deploy
  1. 在设备上运行:

    ¥Run on device:

npx react-native run-ios

如果构建失败,一些故障排除说明将包含在 获取演示 的 "iOS 设备测试" 部分中(步骤 17)。

¥If the build fails, some troubleshooting notes are included in the "iOS Device Testing" part of the Fetch Demo (step 17).

  1. 在设备上下载 https://xlsx.nodejs.cn/pres.numbers

    ¥Download https://xlsx.nodejs.cn/pres.numbers on the device.

  2. 切换回 "SheetJSRN" 应用。

    ¥Switch back to the "SheetJSRN" app.

  3. 从“最近”列表中点击 "导入数据" 并点击 pres

    ¥Tap "Import data" and tap pres from the Recents list.

该表将使用文件中的数据刷新。

¥The table will refresh with data from the file.

  1. 点击 "导出数据",然后在 exportFile 弹出窗口中点击 "OK"。

    ¥Tap "Export Data" and tap "OK" in the exportFile popup.

  2. 从 iOS App Store 安装 "数字" 应用。

    ¥Install the "Numbers" app from the iOS App Store.

  3. 打开 "文件" 应用。重复点击左上角的 < 按钮可返回 "浏览" 视图。

    ¥Open the "Files" app. Repeatedly tap the < button in the top-left corner to return to the "Browse" view.

  4. 点击 "在我的 iPhone 上" 或 "在我的 iPad 上"。点击列表中的 "SheetJSRN"。

    ¥Tap "On My iPhone" or "On My iPad". Tap "SheetJSRN" in the list.

该文件夹中的 sheetjsw 条目是生成的文件。

¥The sheetjsw entry in this folder is the generated file.

  1. 按住 sheetjsw 项目直至出现菜单。选择 "分享"。

    ¥Hold down the sheetjsw item until the menu appears. Select "Share".

  2. 在共享菜单中,联系人列表下方会出现一排应用图标。向左滑动直至出现 "数字" 应用图标,然后点击该应用图标。

    ¥In the sharing menu, below a list of contacts, there will be a row of app icons. Swipe left until the "Numbers" app icon appears and tap the app icon.

Numbers 应用将加载电子表格,确认文件有效。

¥The Numbers app will load the spreadsheet, confirming that the file is valid.

[^1]: 按照 "React Native CLI 快速入门" 并选择适当的 "开发操作系统"。

¥Follow the "React Native CLI Quickstart" and select the appropriate "Development OS".

[^2]: 见 API 参考中的 "数组的数组"

¥See "Array of Arrays" in the API reference

[^3]: 见 "数组输出" 于 "实用函数"

¥See "Array Output" in "Utility Functions"

[^4]: 见 "数组的数组输入" 于 "实用函数"

¥See "Array of Arrays Input" in "Utility Functions"

[^5]: React-Native 提交 5b597b5 添加了 fetch 支持所需的最后一部分。从 0.72.0 开始可在正式版本中使用。

¥React-Native commit 5b597b5 added the final piece required for fetch support. It is available in official releases starting from 0.72.0.

[^6]: 上次测试演示时,Java 17 的 Temurin 发行版是通过运行 brew install temurin17 通过 macOS Brew 包管理器安装的。可在 adoptium.net 直接下载

¥When the demo was last tested, the Temurin distribution of Java 17 was installed through the macOS Brew package manager by running brew install temurin17. Direct downloads are available at adoptium.net

[^7]: 请参阅 React Native 文档中的 "在设备上运行"

¥See "Running On Device" in the React Native documentation

[^8]: 请参阅 Apple 开发者文档中的 UIFileSharingEnabled

¥See UIFileSharingEnabled in the Apple Developer Documentation.

[^9]: 请参阅 Apple 开发者文档中的 LSSupportsOpeningDocumentsInPlace

¥See LSSupportsOpeningDocumentsInPlace in the Apple Developer Documentation.

[^10]: 遵循 Android 版 "React Native CLI 快速入门"(以及 iOS,如果适用)

¥Follow the "React Native CLI Quickstart" for Android (and iOS, if applicable)

[^11]: 请参阅 JDK 存档 for Java 17 JDK 下载链接。

¥See the JDK Archive for Java 17 JDK download links.