rntpc_react-native-zip-archive:基于 OpenHarmony 生态的 React Native 压缩归档工具项目

基于 react-native-zip-archive 的 OpenHarmony 适配版,ZIP 文件压缩与解压

分支7Tags2
文件最后提交记录最后更新时间
10 天前
10 天前
25 天前
3 个月前
6 年前
3 个月前
8 年前
25 天前
3 个月前
3 个月前
25 天前
25 天前
25 天前
25 天前
2 年前
3 个月前
1 个月前
25 天前
25 天前
25 天前
10 天前

文档模板:v0.4.2

react-native-zip-archive

本项目基于 react-native-zip-archive 开发。

该第三方库的仓库已迁移至 Gitcode,并支持直接从 npm 下载,新的包名为:@react-native-ohos/react-native-zip-archive。各版本对应关系如下:

第三方库名称 第三方库版本(npm 地址) 发布信息 支持的 RN 版本 Autolink 编译 API 版本 社区基线版本 源码地址
@react-native-ohos/react-native-zip-archive ~ 9.0.0 Gitcode Releases 0.84.* 是 API12+ 9.0.0 master
@react-native-ohos/react-native-zip-archive ~ 8.0.0 Gitcode Releases 0.82.* 是 API12+ 8.0.0 br_rnoh0.82
@react-native-ohos/react-native-zip-archive ~ 7.1.0 Gitcode Releases 0.77.* 否 API12+ 7.0.2 br_rnoh0.77
@react-native-ohos/react-native-zip-archive ~ 6.1.2 Gitcode Releases 0.72.* 是 API12+ 6.1.1 br_rnoh0.72
@react-native-oh-tpl/react-native-zip-archive <= 6.1.1-0.1.0@deprecated GitHub Releases(已弃用) 0.72.* 否 API12+ 6.1.1 sig

简介

react-native-zip-archive 是一个用于处理 React Native 应用中 ZIP 文件的实用库。它支持对文件或文件夹进行压缩与解压,并可按需添加密码保护。

下载安装

进入工程目录,输入以下命令:

npm

npm install @react-native-ohos/react-native-zip-archive

yarn

yarn add @react-native-ohos/react-native-zip-archive

链接

是否支持 autolink RN框架版本
~ 9.0.0 是 0.84

使用AutoLink的工程需要根据该文档完成配置,Autolink框架指导文档:https://gitcode.com/CPF-RN/ohos_react_native/blob/master/docs/zh-cn/Autolinking.md

如您使用的版本支持 Autolink,并且工程已接入 Autolink,可跳过ManualLink配置。

ManualLink: 本步骤为手动配置原生依赖项的指引

首先需要使用 DevEco Studio 打开项目中的 HarmonyOS 工程 harmony。

1. 覆盖 RN SDK

为了让工程依赖同一版本的 RN SDK,需要在工程根目录的 oh-package.json5 添加 overrides 字段,指向工程所需使用的 RN SDK 版本。用于替换的版本既可以是一个具体的版本号,也可以是一个模糊版本,还可以是本地已存在的 HAR 包或源码目录。

关于该字段的作用,请阅读官方说明

{
  "overrides": {
    "@rnoh/react-native-openharmony": "^0.84.3" // ohpm 在线版本
    // "@rnoh/react-native-openharmony" : "./react_native_openharmony.har" // 指向本地 har 包的路径
    // "@rnoh/react-native-openharmony" : "./react_native_openharmony" // 指向源码路径
  }
}

2. 接入原生端代码

目前有两种方式:

  • 通过 har 包引入;
  • 直接引用源码。

方法一:通过 har 包引入(推荐)

har 包位于三方库安装路径的 `harmony` 文件夹下。

打开 entry/oh-package.json5,添加以下依赖:

"dependencies": {
    "@react-native-ohos/react-native-zip-archive": "file:../../node_modules/@react-native-ohos/react-native-zip-archive/harmony/zipArchive_package.har"
  }

点击右上角的 sync 按钮

或者在命令行终端执行:

cd entry
ohpm install

方法二:直接链接源码

如需使用直接链接源码,请参考[直接链接源码说明](https://gitcode.com/CPF-RN/usage-docs/blob/master/zh-cn/link-source-code.md)

3. 配置 CMakeLists 并引入 RNZipArchivePackage

打开 entry/src/main/cpp/CMakeLists.txt,添加:

project(rnapp)
cmake_minimum_required(VERSION 3.4.1)
set(CMAKE_SKIP_BUILD_RPATH TRUE)
set(RNOH_APP_DIR "${CMAKE_CURRENT_SOURCE_DIR}")
set(NODE_MODULES "${CMAKE_CURRENT_SOURCE_DIR}/../../../../../node_modules")
+ set(OH_MODULES "${CMAKE_CURRENT_SOURCE_DIR}/../../../oh_modules")
set(RNOH_CPP_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../react-native-harmony/harmony/cpp")
set(LOG_VERBOSITY_LEVEL 1)
set(CMAKE_ASM_FLAGS "-Wno-error=unused-command-line-argument -Qunused-arguments")
set(CMAKE_CXX_FLAGS "-fstack-protector-strong -Wl,-z,relro,-z,now,-z,noexecstack -s -fPIE -pie")
set(WITH_HITRACE_SYSTRACE 1) # for other CMakeLists.txt files to use
add_compile_definitions(WITH_HITRACE_SYSTRACE)

add_subdirectory("${RNOH_CPP_DIR}" ./rn)

# RNOH_BEGIN: manual_package_linking_1
add_subdirectory("../../../../sample_package/src/main/cpp" ./sample-package)
+ add_subdirectory("${OH_MODULES}/@react-native-ohos/react-native-zip-archive/src/main/cpp" ./zipArchive-package)
# RNOH_END: manual_package_linking_1

file(GLOB GENERATED_CPP_FILES "./generated/*.cpp")

add_library(rnoh_app SHARED
    ${GENERATED_CPP_FILES}
    "./PackageProvider.cpp"
    "${RNOH_CPP_DIR}/RNOHAppNapiBridge.cpp"
)
target_link_libraries(rnoh_app PUBLIC rnoh)

# RNOH_BEGIN: manual_package_linking_2
target_link_libraries(rnoh_app PUBLIC rnoh_sample_package)
+ target_link_libraries(rnoh_app PUBLIC rnoh_zipArchive)
# RNOH_END: manual_package_linking_2

打开 entry/src/main/cpp/PackageProvider.cpp,添加:

#include "RNOH/PackageProvider.h"
#include "SamplePackage.h"
+ #include "RNZipArchivePackage.h"

using namespace rnoh;

std::vector<std::shared_ptr<Package>> PackageProvider::getPackages(Package::Context ctx) {
    return {
      std::make_shared<SamplePackage>(ctx),
+     std::make_shared<RNZipArchivePackage>(ctx)
    };
}

4. 在 ArkTs 侧引入 RNZipArchivePackage

打开 entry/src/main/ets/RNPackagesFactory.ts,添加:

  ...
+ import {RNZipArchivePackage} from '@react-native-ohos/react-native-zip-archive';

export function createRNPackages(ctx: RNPackageContext): RNPackage[] {
  return [
    new SamplePackage(ctx),
+   new RNZipArchivePackage(ctx)
  ];
}

运行

点击右上角的 sync 按钮

或者在命令行终端执行:

cd entry
ohpm install

然后编译并运行即可。

约束与限制

兼容性

本文档内容已基于以下版本验证通过:

  1. RNOH: 0.84.2; SDK: HarmonyOS 6.0.0 Release SDK; IDE: DevEco Studio 6.0.2.636; ROM: 6.0.0.125;

编译运行API要求

当前第三方库的所有版本均已实现版本隔离,支持在 `API12+` 工程中编译,并在 `API12+` ROM 上运行。

使用示例

以下代码展示了该库的基本使用场景:

使用时,import 的库名保持不变。Demo 中使用的 react-native-blob-util 可参考[react-native-blob-util.md](https://gitcode.com/CPF-RN/usage-docs/blob/master/zh-cn/react-native-blob-util.md?init=initTree)进行配置。

import React, { useState, useEffect, useRef, useCallback, createContext, useContext } from 'react';
import { View, ScrollView, Button, StyleSheet, TextInput, Alert, Text, ActivityIndicator, Keyboard, UIManager, Dimensions } from 'react-native';
import { zip, unzip, zipWithPassword, unzipWithPassword, isPasswordProtected, unzipAssets, getUncompressedSize, subscribe } from 'react-native-zip-archive';
import { Tester, TestSuite, TestCase } from '@rnoh/testerino';
import ReactNativeBlobUtil from 'react-native-blob-util';
const fs = ReactNativeBlobUtil.fs;

type ZipProgressTask = 'zip' | 'passwordZip' | 'unzip';

const KeyboardAvoidContext = createContext<{ onFocusInput?: (event: any) => void }>({});

//demo入口
export default function ZipArchiveDemo() {
    const scrollRef = useRef<ScrollView>(null);
    const scrollYRef = useRef(0);          // 当前 ScrollView 垂直滚动偏移
    const kbHeightRef = useRef(0);         // 最新键盘高度(ref 保证异步回调读到最新值)
    const [kbHeight, setKbHeight] = useState(0);

    useEffect(() => {
        // 鸿蒙(RNOH) 上 KeyboardAvoidingView 通常不生效,这里手动监听键盘事件:
        // 弹起时给 ScrollView 底部留出键盘高度的 padding,并滚动到被聚焦的输入框。
        const showSub = Keyboard.addListener('keyboardDidShow', (e: any) => {
            const h = (e && e.endCoordinates && e.endCoordinates.height) || 320;
            kbHeightRef.current = h;
            setKbHeight(h);
        });
        const hideSub = Keyboard.addListener('keyboardDidHide', () => {
            kbHeightRef.current = 0;
            setKbHeight(0);
        });
        return () => {
            showSub.remove();
            hideSub.remove();
        };
    }, []);

    const onFocusInput = useCallback((event: any) => {
        const target = event?.nativeEvent?.target;
        if (!target) return;
        setTimeout(() => {
            try {
                UIManager.measure(target, (_x: number, _y: number, _w: number, h: number, _pageX: number, pageY: number) => {
                    const win = Dimensions.get('window');
                    const keyboardTop = win.height - kbHeightRef.current; // 键盘上沿在屏幕的 Y 坐标
                    const safeGap = 48;
                    const overflow = pageY + h - (keyboardTop - safeGap); // 输入框底部超出键盘上沿的像素
                    if (overflow > 0) {
                        scrollRef.current?.scrollTo({
                            y: scrollYRef.current + overflow + 12,
                            animated: true,
                        });
                    }
                });
            } catch {
                // measure 不可用时不强制滚动,仅靠 paddingBottom 保证可手动滑出
            }
        }, 300);
    }, []);

    return (
        <KeyboardAvoidContext.Provider value={{ onFocusInput }}>
            <ScrollView
                ref={scrollRef}
                contentContainerStyle={{ flexGrow: 1, paddingBottom: kbHeight }}
                keyboardShouldPersistTaps="handled"
                keyboardDismissMode="interactive"
                scrollEventThrottle={16}
                onScroll={(e) => { scrollYRef.current = e.nativeEvent.contentOffset.y; }}
            >
                <View>
                    <ZipArchiveDemoTest_></ZipArchiveDemoTest_>
                </View>
            </ScrollView>
        </KeyboardAvoidContext.Provider>
    );
};

const ZipArchiveDemoTest_ = () => {
    const [fileName, setFileName] = useState('');
    const [fileContent, setFileContent] = useState('');
    const [createdFilePath, setCreatedFilePath] = useState('');
    const [compressedFilePath, setCompressedFilePath] = useState('');

    const [newZipPath, setNewZipPath]: any = useState();
    const [newSourcePath, setNewSourcePath]: any = useState();
    const [newFolder, setNewFolder]: any = useState();
    const [password, setPassword] = useState('');
    const [showInput, setShowInput] = useState(false);
    const [zipPassword, setZipPassword] = useState('');
    const [zipProgress, setZipProgress] = useState(0);
    const [passwordZipProgress, setPasswordZipProgress] = useState(0);
    const [unzipProgress, setUnzipProgress] = useState(0);
    const [loading, setLoading] = useState(false);
    const [unzipStatus, setUnzipStatus] = useState('');
    const [uncompressSize, setUncompressSize] = useState('');

    // 从外层 ScrollView 拿到键盘避让的聚焦滚动回调(鸿蒙上 KeyboardAvoidingView 不生效)
    const { onFocusInput } = useContext(KeyboardAvoidContext);

    /** 当前进度的 subscribe 句柄,便于切换任务/卸载时彻底清理 */
    const progressSubRef = useRef<{ remove: () => void } | null>(null);
    /** 当前绑定的进度任务,用于把 subscribe 回调里的进度写到对应进度条 */
    const activeTaskRef = useRef<ZipProgressTask | null>(null);

    // 拿到当前任务对应的进度 setter
    const getProgressSetter = useCallback((task: ZipProgressTask) => {
        return task === 'zip'
            ? setZipProgress
            : task === 'passwordZip'
                ? setPasswordZipProgress
                : setUnzipProgress;
    }, []);

    const unbindProgress = useCallback(() => {
        if (progressSubRef.current) {
            progressSubRef.current.remove();
            progressSubRef.current = null;
        }
        activeTaskRef.current = null;
    }, []);

    useEffect(() => {
        return () => {
            unbindProgress();
        };
    }, [unbindProgress]);

    const [unzipPassword, setUnzipPassword] = useState('');

    useEffect(() => {
        console.log('-----pathParameters');
        let filesDir = "/data/storage/el2/base/haps/entry/files"; // 获取鸿蒙应用文件路径
        let newZipPath: any = filesDir + '.zip';
        let newSourcePath: any = filesDir;
        let newFolder: any = filesDir + 'Out';//解压时新建个文件夹

        setNewZipPath(newZipPath);//存储压缩包
        setNewSourcePath(newSourcePath);//原文件路径
        setNewFolder(newFolder);//解压时新建个文件夹

        if (!showInput) {
            setPassword(''); // 隐藏输入框时清空密码
        }
    }, [showInput]);

    // 创建文件
    const createFile = async () => {
        if (!fileName || !fileContent) {
            Alert.alert('文件名和内容不能为空');
            return;
        }
        const filePath = `${newSourcePath}/${fileName}.txt`;

        if (fileName && fileContent) {
            await fs.writeFile(filePath, fileContent, 'utf8');
            setCreatedFilePath(filePath);
            Alert.alert('文件创建成功');
            setTimeout(() => {
                setFileName('');
                setFileContent('');
            }, 100);
        } else {
            Alert.alert('请输入文件名和内容');
        }
        console.log('-----creteFile');
    };

    // 密码压缩
    const handleZipPress = () => {
        if (password === '') {
            Alert.alert('错误', '请输入密码');
            return;
        }

        if (createdFilePath) {
            handleProgress('passwordZip');
            zipWithPassword(newSourcePath, newZipPath, password)
                .then(() => {
                    console.log(`password--11:${password}`)
                    setZipPassword(password);
                    setCompressedFilePath(newZipPath)
                    finishProgress('passwordZip');
                    Alert.alert('成功', '已使用密码创建压缩');
                })
                .catch(error => {
                    unbindProgress();
                    Alert.alert('错误', `创建压缩文件失败: ${error}`);
                });
            console.log('-----zipWithPassword');
        } else {
            Alert.alert('无文件可供压缩');
        }
    };

    // 解压时是否需要密码
    const isUnzipWithPassword = () => {
        if (unzipPassword) {
            if (unzipPassword === zipPassword) {
                handleGetUncompressedSize();
                handleProgress('unzip');
                unzipWithPassword(newZipPath, newFolder, unzipPassword)
                    .then(() => {
                        finishProgress('unzip');
                        setShowInput(false);
                        Alert.alert('成功', '已使用密码解压文件');
                    })
                    .catch(error => {
                        unbindProgress();
                        Alert.alert('错误', `解压文件失败: ${error}`);
                    });
                console.log('-----unzipWithPassword');
            } else {
                Alert.alert('密码输入错误');
            }
        } else {
            Alert.alert('错误', '请先输入密码');
        }
    }

    // 密码解压&解压
    const handleUnzipPress = () => {
        isPasswordProtected(newZipPath)
            .then((res) => {
                if (res) {
                    // 有密码:弹出密码输入,走密码解压分支
                    setShowInput(true);
                    isUnzipWithPassword();
                    return 'success';
                } else {
                    // 无密码:直接解压
                    if (compressedFilePath) {
                        setShowInput(false);
                        handleGetUncompressedSize();
                        handleProgress('unzip');
                        unzip(newZipPath, newFolder, 'UTF-8')
                            .then(() => {
                                console.log(`unzip success`)
                                finishProgress('unzip');
                                Alert.alert('成功', '已解压');
                                return 'success';
                            })
                            .catch(error => {
                                unbindProgress();
                                Alert.alert('错误', '解压失败');
                                console.log(`unzip error: ${error}`);
                                return 'failed';
                            })
                        console.log('-----unzip');
                        return 'success';
                    } else {
                        Alert.alert('无压缩文件可供解压');
                        return 'failed';
                    }
                }
            })
            .catch(error => {
                Alert.alert('无压缩文件可供解压');
                return 'failed';
            })
        console.log('-----isPasswordProtected');
        return 'success';
    }

    // 仅更新与当前操作对应的进度条。使用 subscribe 监听 progress 事件 (0~1),转换成百分比写入 state。
    const handleProgress = (task: ZipProgressTask) => {
        unbindProgress();

        const setForTask = getProgressSetter(task);
        setForTask(0);
        activeTaskRef.current = task;

        const sub = subscribe(({ progress: p, filePath }: { progress: number, filePath: string }) => {
            const currentTask = activeTaskRef.current;
            if (currentTask !== task) {
                return;
            }
            const pct = Math.round(p * 100);
            const setter = getProgressSetter(currentTask);
            setter(pct);
            console.log(`[${task}] progress event: ${pct}%`);
        });
        progressSubRef.current = sub;
    }

    // 操作成功后强制把进度条拉满 100%(鸿蒙侧 progress 事件可能不派发到 JS)
    const finishProgress = (task: ZipProgressTask) => {
        const setForTask = getProgressSetter(task);
        setForTask(100);
        unbindProgress();
    }

    // unzipAssets解压到指定目录
    const handleUnzipAssets = async () => {
        setLoading(true);
        let filesDir = "/data/storage/el2/base/haps/entry/files";
        //解压files.zip文件到系统中的 destinationFolder 目录中
        let assetPath = filesDir + '.zip';
        let targetPath = filesDir + 'destinationFolder';
        if (compressedFilePath) {
            try {
                await unzipAssets(assetPath, targetPath);
                setUnzipStatus('解压完成');
                console.log(`unzipAssets success`);
            } catch (err) {
                setUnzipStatus(`解压失败:${err}`);
                console.log(`unzipAssets err: ${err}`);
            } finally {
                setLoading(false);
            }
            console.log('-----unzipAssets');
        } else {
            Alert.alert('无压缩文件可供解压');
        }
    }

    // getUncompressedSize解压文件大小
    const handleGetUncompressedSize = () => {
        getUncompressedSize(newZipPath)
            .then((uncompressSize: any) => {
                setUncompressSize(uncompressSize);
                console.log(`uncompressSize success:${uncompressSize}`)
            })
            .catch((err) => {
                console.log(`getUncompressedSize err:${err}`)
            })
        console.log('-----getUncompressedSize');
    }

    return (
        <Tester>
            <TestSuite name="创建文件">
                <TestCase tags={['C_API']} itShould="创建文件"
                    initialState={''}
                    arrange={({ setState }) =>
                        <View >
                            <View >
                                <TextInput
                                    placeholder="请输入文件名"
                                    value={fileName}
                                    onChangeText={setFileName}
                                    onFocus={onFocusInput}
                                    style={{ borderWidth: 1, padding: 10, width: '70%' }}
                                />
                                <TextInput
                                    style={styles.input}
                                    onChangeText={setFileContent}
                                    value={fileContent}
                                    placeholder="文件内容"
                                    onFocus={onFocusInput}
                                />
                                <Button title="创建文件" onPress={() => { Keyboard.dismiss(); createFile(); setState('success'); }} />
                            </View>
                        </View>
                    }
                    assert={({ expect, state }) => {
                        expect(state).to.be.eq('success');
                    }}>
                </TestCase>
            </TestSuite>
            <TestSuite name="压缩文件">
                <TestCase tags={['C_API']} itShould="压缩文件"
                    initialState={''}
                    arrange={({ setState }) =>
                        <View style={styles.buttonSix}>
                            <Text >压缩进度</Text>
                            <View style={styles.progressBar}>
                                <View style={{ width: `${zipProgress}%`, backgroundColor: '#00AEEF', height: '100%' }}></View>
                            </View>
                            <Text style={styles.percentageText}>{zipProgress}%</Text>
                            <Button title='压缩' onPress={() => {
                                Keyboard.dismiss();
                                if (createdFilePath) {
                                    handleProgress('zip');
                                    zip(newSourcePath, newZipPath)
                                        .then(() => {
                                            setCompressedFilePath(newZipPath)
                                            finishProgress('zip');
                                            Alert.alert('成功', '已压缩');
                                            setState('success');
                                        })
                                        .catch(error => {
                                            unbindProgress();
                                            Alert.alert('错误', `压缩失败: ${error}`);
                                        })
                                    console.log('-----zip');
                                } else {
                                    Alert.alert('无文件可供压缩');
                                }
                            }} />
                        </View>
                    }
                    assert={({ expect, state }) => {
                        expect(state).to.be.eq('success');
                    }}>
                </TestCase>
            </TestSuite>
            <TestSuite name="设置密码压缩文件">
                <TestCase tags={['C_API']} itShould="设置密码压缩文件"
                    initialState={''}
                    arrange={({ setState }) =>
                        <View>
                            <Text >压缩进度</Text>
                            <View style={styles.progressBar}>
                                <View style={{ width: `${passwordZipProgress}%`, backgroundColor: '#00AEEF', height: '100%' }}></View>
                            </View>
                            <Text style={styles.percentageText}>{passwordZipProgress}%</Text>
                            <TextInput
                                style={styles.input}
                                placeholder="设置压缩密码"
                                onChangeText={setPassword}
                                value={password}
                                onFocus={onFocusInput}
                            />
                            <View style={styles.buttonSix}>
                                <Button title='密码压缩' onPress={() => { Keyboard.dismiss(); handleZipPress(); setState('success'); }} />
                            </View>
                        </View>
                    }
                    assert={({ expect, state }) => {
                        expect(state).to.be.eq('success');
                    }}>
                </TestCase>
            </TestSuite>
            <TestSuite name="解压文件">
                <TestCase tags={['C_API']} itShould="解压文件"
                    initialState={''}
                    arrange={({ setState }) =>
                        <View>
                            <Text >压缩进度</Text>
                            <View style={styles.progressBar}>
                                <View style={{ width: `${unzipProgress}%`, backgroundColor: '#00AEEF', height: '100%' }}></View>
                            </View>
                            <Text style={styles.percentageText}>{unzipProgress}%</Text>
                            <TextInput
                                style={styles.input}
                                placeholder="输入解压密码"
                                value={unzipPassword}
                                onChangeText={setUnzipPassword}
                                onFocus={onFocusInput}
                            />
                            <View style={styles.buttonSix}>
                                <Button title="解压" onPress={() => { Keyboard.dismiss(); var result = handleUnzipPress(); setState(result); }} />
                            </View>
                            <Text>解压缩后的大小:{uncompressSize ? uncompressSize : '0'}字节</Text>
                        </View>
                    }
                    assert={({ expect, state }) => {
                        expect(state).to.be.eq('success');
                    }}>
                </TestCase>
            </TestSuite>
            <TestSuite name="unzipAssets解压">
                <TestCase tags={['C_API']} itShould="解压到指定目录"
                    initialState={''}
                    arrange={({ setState }) =>
                        <View>
                            <Button title='unzipAssets解压' onPress={() => { Keyboard.dismiss(); handleUnzipAssets(); setState('success'); }} />
                            {loading ? (
                                <View>
                                    <ActivityIndicator size="large" color='#0000ff' />
                                    <Text>正在解压文件...</Text>
                                </View>
                            ) : (
                                <Text>{unzipStatus}</Text>
                            )}
                        </View>
                    }
                    assert={({ expect, state }) => {
                        expect(state).to.be.eq('success');
                    }}>
                </TestCase>
            </TestSuite>
        </Tester>
    )
}

const styles = StyleSheet.create({
    content: {
        display: 'flex',
        justifyContent: 'center',
        alignItems: 'center',
        marginTop: 56,
        backgroundColor: 'white'
    },
    buttonSix: {
        width: '65%',
        marginBottom: 10,
        marginTop: 20
    },
    input: {
        borderWidth: 1,
        padding: 10,
        width: 300
    },
    container: {
        flex: 1,
        justifyContent: 'center',

        alignItems: 'center',
    },
    progressBar: {
        width: 250,
        height: 20,
        backgroundColor: '#E0E0E0',
        borderRadius: 10,
        overflow: 'hidden',
        marginTop: 10
    },
    percentageText: {
        marginTop: 5,
        fontSize: 16
    }
})

使用说明

以下示例中的 `sourcePath`、`targetPath` 等路径须为鸿蒙应用可访问的沙箱路径,例如 `/data/storage/el2/base/haps/entry/files`。路径中的 `file://` 前缀会自动移除,无需手动处理。

压缩文件或文件夹

import { zip } from 'react-native-zip-archive';

// 压缩单个文件或文件夹
zip(sourcePath, targetZipPath)
  .then((path) => console.log('压缩完成:', path))
  .catch((err) => console.log('压缩失败:', err));

// 压缩多个文件:source 传数组
zip([file1, file2, file3], targetZipPath)
  .then((path) => console.log('多文件压缩完成:', path));

// 指定压缩等级:0=不压缩,1=最快,9=最高压缩,-1=默认
zip(sourcePath, targetZipPath, 9).then(...);

文件解压

import { unzip } from 'react-native-zip-archive';

// 解压到指定目录,charset 默认 'UTF-8'
unzip(zipPath, targetDir)
  .then((path) => console.log('解压完成:', path));

// 指定编码(处理非 UTF-8 文件名)
unzip(zipPath, targetDir, 'GBK').then(...);

密码压缩与解压

import { zipWithPassword, unzipWithPassword, isPasswordProtected } from 'react-native-zip-archive';

// 密码压缩:encryptionMethod 可选,compressionLevel 可选
zipWithPassword(sourcePath, targetZipPath, '123456')
  .then((path) => console.log('密码压缩完成:', path));

// 密码压缩多文件并指定加密方式
zipWithPassword([file1, file2], targetZipPath, '123456', 'AES-256', 9).then(...);

// 解压前先判断是否加密,再决定走密码解压还是普通解压
isPasswordProtected(zipPath).then((encrypted) => {
  if (encrypted) {
    unzipWithPassword(zipPath, targetDir, '123456')
      .then((path) => console.log('密码解压完成:', path));
  } else {
    unzip(zipPath, targetDir).then(...);
  }
});

将资源文件解压到指定目录

import { unzipAssets } from 'react-native-zip-archive';

// 将资源 zip 解压到目标目录(部分平台不支持,会抛出 "unzipAssets not supported on this platform")
unzipAssets(assetPath, targetDir)
  .then((path) => console.log('解压完成:', path))
  .catch((err) => console.log('解压失败:', err));

获取解压后文件大小

import { getUncompressedSize } from 'react-native-zip-archive';

// 返回解压后总大小(字节),charset 默认 'UTF-8'
getUncompressedSize(zipPath)
  .then((size) => console.log('解压后大小:', size, '字节'));

监听压缩与解压进度

import { subscribe, zip } from 'react-native-zip-archive';

// subscribe 返回一个订阅句柄,progress 取值 0~1
const sub = subscribe(({ progress, filePath }) => {
  console.log(`${filePath}: ${Math.round(progress * 100)}%`);
});

zip(sourcePath, targetZipPath)
  .then(() => {
    console.log('完成');
    sub.remove(); // 操作结束后移除监听,避免内存泄漏
  })
  .catch(() => sub.remove());

`subscribe` 返回的是全局事件监听,多个压缩/解压任务会复用同一事件通道。切换任务或组件卸载时,请务必调用 `sub.remove()` 进行清理;否则回调可能会串扰到错误任务的进度展示。

接口说明

"Platform" 列表示该属性在原第三方库中支持的平台。

"OpenHarmony Support" 列为 yes 时,表示 OpenHarmony 平台支持该属性;no 表示不支持;partially 表示部分支持。其使用方法跨平台保持一致,效果与 iOS 或 Android 对齐。

API

名称 类型 参数类型 返回值 必填 平台 OpenHarmony平台支持 描述
zip function (source: string | string[], target: string, compressionLevel?: number) Promise<string> 否 全部 是 压缩文件或目录;当 source 传入数组时,可压缩多个文件。compressionLevel 默认值为 -1,取值范围为 -1 到 9,成功后返回压缩文件的路径。
unzip function (source: string, target: string, charset?: string) Promise<string> 否 全部 是 解压到 target 目录。charset 用于解码压缩包内文件名,默认值为 UTF-8,支持 UTF-8/GBK/GB2312/Big5/Shift_JIS/sjis。
zipWithPassword function (source: string | string[], target: string, password: string, encryptionMethod?: string, compressionLevel?: number) Promise<string> 否 全部 是 使用密码进行压缩。encryptionMethod 支持 STANDARD/AES-128/AES-256;compressionLevel 默认值为 -1,取值范围为 -1 到 9,成功后返回压缩文件的路径。
unzipWithPassword function (source: string, target: string, password: string) Promise<string> 否 全部 是 使用密码进行解压。
isPasswordProtected function (source: string) Promise<boolean> 否 全部 是 检查压缩包是否设置了密码保护。
unzipAssets function (source: string, target: string) Promise<string> 否 全部 是 将资源 zip 文件解压到指定目录。
getUncompressedSize function (source: string, charset?: string) Promise<number> 否 全部 是 获取压缩包解压后的总大小(字节)。charset 默认值为 UTF-8,支持 UTF-8/GBK/GB2312/Big5/Shift_JIS/sjis;读取失败时返回 -1。
subscribe function (callback: ({ progress, filePath }: { progress: number, filePath: string }) => void) NativeEventSubscription 否 全部 是 订阅压缩/解压进度事件。回调参数为 { progress, filePath },其中 progress 取值范围为 0 到 1,filePath 为当前正在处理的 zip 文件路径。返回 NativeEventSubscription(包含 remove() 方法);该事件为全局单通道且多任务共享,切换任务或卸载组件时须调用 remove() 清理。

枚举

名称 值 平台 HarmonyOS 平台支持 描述
DEFAULT_COMPRESSION -1 全部 支持 默认压缩等级
NO_COMPRESSION 0 全部 支持 只打包,不压缩
BEST_SPEED 1 全部 支持 最快速度压缩
BEST_COMPRESSION 9 全部 支持 最高压缩等级

遗留问题

无

其他

  1. 编译报错“Duplicated files found in module entry. This may cause unexpected errors at runtime. ERROR: 2 file found in 'lin\arm64-v8a\libz.so.1'...”

解决方案:该报错是由于从不同包中收集到同名 so 包(名称为 libz.so.1),导致 so 包冲突。可在模块级 build-profile.json5 文件中添加以下配置:

"buildOption": {
  "nativeLib": {
    "filter": {
      "pickFirsts": ["**/libz.so.1"]
    }
  }
}

目录结构

/rntpc_react-native-zip-archive  # 项目根目录
├── harmony                          # 鸿蒙适配代码
│    └─ zipArchive_package.har          # har 包
│    └─ zipArchive_package             # 鸿蒙适配核心代码
│          └─ Index.ets                # 鸿蒙适配代码入口
│          └─ ts.ets                   # ArkTS 侧类型导出入口
│          └─ libs/arm64-v8a           # 原生 so 库(libz/libminizip/liblzma/libzstd)
│          └─ src/main
│              └─ ets
│                  └─ RNZipArchiveModule.ets   # 鸿蒙侧 TurboModule 实现
│                  └─ RNZipArchivePackage.ets  # 鸿蒙侧 Package
│              └─ cpp
│                  └─ RNZipArchiveModule.cpp/.h  # C++ 侧 TurboModule 实现
│                  └─ RNZipArchivePackage.h      # C++ 侧 Package
│                  └─ generated                  # codegen 生成代码
│                  └─ thirdparty/minizip-ng     # 压缩第三方库
├── index.js                          # RN 侧入口(导出 zip/unzip 等 API)
├── index.d.ts                        # RN 侧类型定义
├── specs
│    └─ NativeZipArchive.ts            # codegen TurboModule 规范定义
├── example                           # 示例工程
├── README.md                         # 中文文档
├── README_en.md                      # 英文文档

贡献代码

如您在使用过程中遇到任何问题,欢迎提交 Issue,也诚挚欢迎提交 PR 。

开源协议

本项目基于 The MIT License (MIT) ,欢迎自由使用并参与开源共建。

项目介绍

基于 react-native-zip-archive 的 OpenHarmony 适配版,ZIP 文件压缩与解压

定制我的领域