React Native for OpenHarmony 实战:文本加密实现

在日常开发中,我们经常需要对文本进行编码和解码。Base64 是最常用的编码方式之一,它可以把任意二进制数据转换成可打印的 ASCII 字符。今天我们用 React Native 实现一个文本加密工具,支持 Base64 编码和解码,还带有流畅的切换动画。
Base64 编码原理
Base64 不是加密算法,而是一种编码方式。它的核心思想是把 3 个字节(24 位)的数据分成 4 组,每组 6 位,然后用 64 个可打印字符来表示这 6 位数据。
为什么要用 Base64?因为有些场景只能传输文本数据,比如 JSON、XML、URL 参数等。如果要传输二进制数据(图片、文件等),就需要先编码成文本。
Base64 使用的 64 个字符是:A-Z、a-z、0-9、+、/,再加上填充字符 =。
状态设计
import React, { useState, useRef, useEffect } from 'react';
import { View, Text, TextInput, TouchableOpacity, StyleSheet, ScrollView, Animated } from 'react-native';
export const TextEncrypt: React.FC = () => {
const [input, setInput] = useState('');
const [output, setOutput] = useState('');
const [mode, setMode] = useState<'encode' | 'decode'>('encode');
const scaleAnim = useRef(new Animated.Value(1)).current;
const outputAnim = useRef(new Animated.Value(0)).current;
const tabAnim = useRef(new Animated.Value(0)).current;
状态变量的设计很简洁:
输入和输出 input 和 output:分别存储用户输入的文本和处理后的结果。初始都是空字符串。
模式 mode:用 TypeScript 的联合类型限定只能是 'encode' 或 'decode',避免拼写错误。默认是编码模式。
动画值:
scaleAnim:按钮按下时的缩放动画,初始值 1 表示正常大小outputAnim:结果区域的弹出动画,初始值 0 表示隐藏状态tabAnim:标签页切换时的滑动动画,0 表示左边,1 表示右边
为什么用三个独立的动画值?因为它们控制不同的元素,需要独立触发。如果用一个动画值控制多个元素,就无法实现不同的动画效果。
标签页切换动画
useEffect(() => {
Animated.timing(tabAnim, {
toValue: mode === 'encode' ? 0 : 1,
duration: 200,
useNativeDriver: false,
}).start();
}, [mode]);
这个 useEffect 监听 mode 的变化,当用户切换编码/解码模式时,自动触发标签指示器的滑动动画。
动画目标值:编码模式时滑到左边(0),解码模式时滑到右边(1)。
动画时长:200 毫秒,这是一个比较舒适的速度,不会太快也不会太慢。
useNativeDriver:这里设置为 false,因为我们要动画化 left 属性(位置),而原生驱动只支持 transform 和 opacity。虽然性能稍差,但对于这种简单动画影响不大。
为什么用 useEffect 而不是在按钮点击时触发?因为 mode 可能在其他地方被修改,用 useEffect 可以确保动画总是和状态同步。
Base64 编码实现
const base64Encode = (str: string) => {
try {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
const bytes = new TextEncoder().encode(str);
let result = '';
Base64 编码的第一步是把字符串转换成字节数组。TextEncoder 是浏览器和 Node.js 都支持的标准 API,它把字符串按 UTF-8 编码转换成字节。
字符表 chars:这 64 个字符就是 Base64 的"字母表"。索引 0-63 分别对应这 64 个字符。
为什么用 try-catch:编码过程可能失败,比如输入包含无法编码的字符。捕获异常后返回友好的错误提示,避免程序崩溃。
编码核心逻辑
for (let i = 0; i < bytes.length; i += 3) {
const b1 = bytes[i], b2 = bytes[i + 1] || 0, b3 = bytes[i + 2] || 0;
result += chars[b1 >> 2] + chars[((b1 & 3) << 4) | (b2 >> 4)] +
(i + 1 < bytes.length ? chars[((b2 & 15) << 2) | (b3 >> 6)] : '=') +
(i + 2 < bytes.length ? chars[b3 & 63] : '=');
}
return result;
} catch { return '编码失败'; }
};
这是 Base64 编码的核心算法,每次处理 3 个字节(24 位),转换成 4 个 Base64 字符。
循环步长:i += 3 表示每次跳 3 个字节。如果字节数不是 3 的倍数,最后一组会不足 3 个字节。
读取字节:b1、b2、b3 分别是当前组的三个字节。如果不足 3 个,用 || 0 补零。
位运算拆分:
- 第 1 个字符:取
b1的高 6 位 →b1 >> 2 - 第 2 个字符:取
b1的低 2 位和b2的高 4 位 →((b1 & 3) << 4) | (b2 >> 4) - 第 3 个字符:取
b2的低 4 位和b3的高 2 位 →((b2 & 15) << 2) | (b3 >> 6) - 第 4 个字符:取
b3的低 6 位 →b3 & 63
填充处理:如果原始数据不足 3 个字节,用 = 填充。i + 1 < bytes.length 判断是否有第 2 个字节,没有就用 = 代替第 3 个字符。同理,第 4 个字符也可能是 =。
这个算法看起来复杂,但本质就是把 24 位数据重新分组成 4 个 6 位数据。位运算是最高效的实现方式。
Base64 解码实现
const base64Decode = (str: string) => {
try {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
str = str.replace(/=+$/, '');
const bytes: number[] = [];
解码是编码的逆过程,把 Base64 字符串还原成字节数组,再转换成原始字符串。
去除填充:replace(/=+$/, '') 删除末尾的 = 字符。填充只是为了凑齐 4 的倍数,解码时不需要。
字节数组:用 number[] 存储解码后的字节,最后再转换成字符串。
解码核心逻辑
for (let i = 0; i < str.length; i += 4) {
const b1 = chars.indexOf(str[i]), b2 = chars.indexOf(str[i + 1]);
const b3 = chars.indexOf(str[i + 2]), b4 = chars.indexOf(str[i + 3]);
bytes.push((b1 << 2) | (b2 >> 4));
if (b3 >= 0) bytes.push(((b2 & 15) << 4) | (b3 >> 2));
if (b4 >= 0) bytes.push(((b3 & 3) << 6) | b4);
}
return new TextDecoder().decode(new Uint8Array(bytes));
} catch { return '解码失败'; }
};
解码算法每次处理 4 个 Base64 字符,还原成 3 个字节。
查找索引:chars.indexOf(str[i]) 把字符转换回 0-63 的索引值。如果字符不在字符表中,返回 -1。
位运算合并:
- 第 1 个字节:取
b1的全部 6 位和b2的高 2 位 →(b1 << 2) | (b2 >> 4) - 第 2 个字节:取
b2的低 4 位和b3的高 4 位 →((b2 & 15) << 4) | (b3 >> 2) - 第 3 个字节:取
b3的低 2 位和b4的全部 6 位 →((b3 & 3) << 6) | b4
边界检查:if (b3 >= 0) 判断第 3 个字符是否存在(不是填充)。如果是 -1,说明原始数据只有 1 个字节,不需要生成第 2、3 个字节。
转换字符串:TextDecoder 把字节数组按 UTF-8 解码成字符串。Uint8Array 是 JavaScript 的类型化数组,专门用于处理二进制数据。
这个算法和编码算法是对称的,理解了编码,解码就很容易理解了。
鸿蒙 ArkTS 对比:编解码实现
import util from '@ohos.util'
base64Encode(str: string): string {
try {
const textEncoder = new util.TextEncoder()
const bytes = textEncoder.encodeInto(str)
const base64 = new util.Base64Helper()
return base64.encodeToStringSync(bytes)
} catch {
return '编码失败'
}
}
base64Decode(str: string): string {
try {
const base64 = new util.Base64Helper()
const bytes = base64.decodeSync(str)
const textDecoder = util.TextDecoder.create('utf-8')
return textDecoder.decodeWithStream(bytes)
} catch {
return '解码失败'
}
}
鸿蒙系统提供了 util.Base64Helper 工具类,可以直接调用,不需要手动实现位运算。这是系统级 API,性能更好,代码也更简洁。
但我们的手动实现也有优势:跨平台兼容性好,不依赖特定平台的 API。而且理解了算法原理,遇到问题时更容易调试。
处理函数
const process = () => {
Animated.sequence([
Animated.timing(scaleAnim, { toValue: 0.95, duration: 100, useNativeDriver: true }),
Animated.spring(scaleAnim, { toValue: 1, friction: 3, useNativeDriver: true }),
]).start();
outputAnim.setValue(0);
Animated.spring(outputAnim, { toValue: 1, friction: 5, useNativeDriver: true }).start();
setOutput(mode === 'encode' ? base64Encode(input) : base64Decode(input));
};
点击编码/解码按钮时触发这个函数,它做了三件事:
按钮动画:先缩小到 95%(100ms),再弹回原大小。Animated.spring 创建弹簧动画,friction: 3 设置摩擦力,值越小弹性越大。这个动画给用户"按下按钮"的触感反馈。
结果动画:重置 outputAnim 为 0(隐藏状态),然后弹出到 1(显示状态)。friction: 5 让弹出效果更明显。
执行编解码:根据当前模式调用对应的函数,把结果存入 output。三元运算符让代码更简洁。
为什么先触发动画再设置结果?因为动画是异步的,start() 不会阻塞代码执行。如果先设置结果,界面会立即更新,动画效果就不明显了。
标签页插值
const tabPosition = tabAnim.interpolate({ inputRange: [0, 1], outputRange: ['0%', '50%'] });
interpolate 把动画值映射成实际的位置:
- 动画值 0 → 位置 0%(左边)
- 动画值 1 → 位置 50%(右边)
为什么用百分比而不是像素?因为标签页的宽度是动态的,用百分比可以自适应不同屏幕尺寸。50% 正好是两个标签页的分界线。
界面渲染:头部
return (
<ScrollView style={styles.container}>
<View style={styles.header}>
<Text style={styles.headerIcon}>🔐</Text>
<Text style={styles.headerTitle}>文本加密</Text>
<Text style={styles.headerSubtitle}>Base64 编码/解码</Text>
</View>
头部区域包含三个元素:
图标 🔐:锁的 emoji 表示"加密"的概念,字号 50 很醒目。
标题:28 号粗体白色文字,是页面的主标题。
副标题:14 号灰色文字,说明工具的功能。副标题比标题小,颜色也更淡,形成视觉层次。
用 ScrollView 包裹整个页面,因为输入和输出的文本可能很长,需要滚动查看。
标签页切换
<View style={styles.tabContainer}>
<Animated.View style={[styles.tabIndicator, { left: tabPosition }]} />
<TouchableOpacity style={styles.tab} onPress={() => setMode('encode')} activeOpacity={0.7}>
<Text style={[styles.tabText, mode === 'encode' && styles.tabTextActive]}>🔒 编码</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.tab} onPress={() => setMode('decode')} activeOpacity={0.7}>
<Text style={[styles.tabText, mode === 'decode' && styles.tabTextActive]}>🔓 解码</Text>
</TouchableOpacity>
</View>
标签页的实现很巧妙:
容器:深色背景,圆角 12,内边距 4,形成一个"轨道"。
指示器:蓝色背景的 Animated.View,绝对定位,宽度 50%。left 属性绑定动画值,实现滑动效果。
标签按钮:两个按钮平分宽度(flex: 1),zIndex: 1 让它们显示在指示器上方。点击时切换 mode 状态。
文字样式:默认灰色,选中时白色加粗。mode === 'encode' && styles.tabTextActive 是条件样式的常用写法。
图标:🔒 表示编码(锁上),🔓 表示解码(解锁),直观易懂。
这种标签页设计在很多 App 中都能看到,用户很熟悉,不需要学习成本。
鸿蒙 ArkTS 对比:标签页
Tabs({ index: this.mode === 'encode' ? 0 : 1 }) {
TabContent() {
// 编码界面
}.tabBar('🔒 编码')
TabContent() {
// 解码界面
}.tabBar('🔓 解码')
}
.barMode(BarMode.Fixed)
.barBackgroundColor('#1a1a3e')
.onChange((index: number) => {
this.mode = index === 0 ? 'encode' : 'decode'
})
鸿蒙提供了 Tabs 组件,自带滑动指示器和切换动画,不需要手动实现。但灵活性不如自定义实现,比如指示器的样式、动画效果等都受限于组件的设计。
输入区域
<View style={styles.inputCard}>
<Text style={styles.label}>{mode === 'encode' ? '📝 原文' : '🔤 Base64'}</Text>
<TextInput
style={styles.input}
value={input}
onChangeText={setInput}
multiline
placeholder="输入文本..."
placeholderTextColor="#666"
/>
</View>
输入区域包含标签和输入框:
动态标签:根据模式显示不同的文字。编码模式下是"原文",解码模式下是"Base64"。这样用户一眼就知道应该输入什么。
多行输入框:multiline 属性让输入框支持换行,适合输入长文本。minHeight: 120 确保输入框有足够的高度。
占位符:灰色的提示文字,输入内容后自动消失。placeholderTextColor 自定义占位符颜色,默认的颜色在深色背景上不够明显。
受控组件:value 和 onChangeText 绑定状态,这是 React 的标准模式。输入框的内容完全由状态控制,方便做验证、格式化等操作。
处理按钮
<Animated.View style={{ transform: [{ scale: scaleAnim }] }}>
<TouchableOpacity style={styles.btn} onPress={process} activeOpacity={0.8}>
<Text style={styles.btnIcon}>{mode === 'encode' ? '🔒' : '🔓'}</Text>
<Text style={styles.btnText}>{mode === 'encode' ? '编码' : '解码'}</Text>
</TouchableOpacity>
</Animated.View>
按钮用 Animated.View 包裹,应用缩放动画。点击时会触发 process 函数中的动画序列。
动态内容:图标和文字都根据模式动态变化。编码时显示 🔒 和"编码",解码时显示 🔓 和"解码"。
横向布局:flexDirection: 'row' 让图标和文字水平排列,alignItems: 'center' 垂直居中。
视觉反馈:activeOpacity={0.8} 让按钮按下时稍微变暗,配合缩放动画,给用户明确的触摸反馈。
输出区域
{output ? (
<Animated.View style={[styles.outputCard, {
transform: [{ scale: outputAnim }, { perspective: 1000 }],
opacity: outputAnim,
}]}>
<Text style={styles.label}>{mode === 'encode' ? '🔤 Base64' : '📝 原文'}</Text>
<View style={styles.outputBox}>
<Text style={styles.output} selectable>{output}</Text>
</View>
</Animated.View>
) : null}
</ScrollView>
);
};
输出区域只在有结果时才显示,用条件渲染实现。
弹出动画:同时应用缩放和透明度动画。scale 从 0 到 1,opacity 也从 0 到 1,形成"弹出"效果。
透视效果:perspective: 1000 添加 3D 透视,让缩放动画更有立体感。这是一个细节优化,让动画看起来更自然。
动态标签:和输入区域相反,编码模式下显示"Base64",解码模式下显示"原文"。
可选择文本:selectable 属性让用户可以长按选择复制结果,这是一个很重要的功能。如果不能复制,用户就得手动输入,体验很差。
等宽字体:fontFamily: 'monospace' 让 Base64 字符串更易读,每个字符占据相同宽度。
样式定义:容器和头部
const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: '#0f0f23', padding: 20 },
header: { alignItems: 'center', marginBottom: 24 },
headerIcon: { fontSize: 50, marginBottom: 8 },
headerTitle: { fontSize: 28, fontWeight: '700', color: '#fff' },
headerSubtitle: { fontSize: 14, color: '#888', marginTop: 4 },
容器用深蓝色背景 #0f0f23,这是一个很流行的深色主题配色,比纯黑色更柔和。
头部元素全部居中对齐,图标、标题、副标题之间有适当的间距,形成清晰的视觉层次。
样式定义:标签页
tabContainer: {
flexDirection: 'row',
backgroundColor: '#1a1a3e',
borderRadius: 12,
padding: 4,
marginBottom: 20,
position: 'relative',
},
tabIndicator: {
position: 'absolute',
width: '50%',
height: '100%',
backgroundColor: '#4A90D9',
borderRadius: 10,
top: 4,
},
tab: { flex: 1, paddingVertical: 12, alignItems: 'center', zIndex: 1 },
tabText: { color: '#888', fontSize: 16 },
tabTextActive: { color: '#fff', fontWeight: '600' },
标签容器用 position: 'relative',让指示器可以绝对定位。内边距 4 在指示器和边缘之间留出空隙。
指示器的圆角 10 比容器的圆角 12 小一点,这样四个角不会超出容器。top: 4 对应容器的内边距。
标签按钮用 zIndex: 1 确保显示在指示器上方,否则文字会被蓝色背景遮挡。
样式定义:输入输出和按钮
inputCard: {
backgroundColor: '#1a1a3e',
borderRadius: 20,
padding: 20,
marginBottom: 20,
borderWidth: 1,
borderColor: '#3a3a6a',
},
label: { color: '#888', fontSize: 14, marginBottom: 12 },
input: {
backgroundColor: '#252550',
padding: 16,
borderRadius: 12,
minHeight: 120,
color: '#fff',
fontSize: 16,
},
btn: {
backgroundColor: '#4A90D9',
borderRadius: 16,
paddingVertical: 18,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
marginBottom: 20,
shadowColor: '#4A90D9',
shadowOffset: { width: 0, height: 8 },
shadowOpacity: 0.4,
shadowRadius: 15,
},
btnIcon: { fontSize: 20, marginRight: 10 },
btnText: { color: '#fff', fontSize: 18, fontWeight: '700' },
outputCard: {
backgroundColor: '#1a1a3e',
borderRadius: 20,
padding: 20,
borderWidth: 1,
borderColor: '#3a3a6a',
},
outputBox: {
backgroundColor: '#252550',
padding: 16,
borderRadius: 12,
minHeight: 100,
},
output: { color: '#9cdcfe', fontFamily: 'monospace', fontSize: 14 },
});
输入和输出卡片用相同的样式,保持视觉一致性。深色背景 #1a1a3e 和边框 #3a3a6a 形成层次感。
输入框和输出框用更深的背景色 #252550,和卡片背景形成对比,让用户知道这是可交互的区域。
按钮用蓝色背景和蓝色阴影,是页面的视觉焦点。阴影向下偏移 8 像素,模拟"悬浮"效果。
输出文本用青蓝色 #9cdcfe,这是 VS Code 中字符串的颜色,程序员看着很亲切。
小结
这个文本加密工具展示了 Base64 编解码的完整实现。虽然鸿蒙系统提供了现成的 API,但手动实现让我们理解了编码的原理,也提高了代码的跨平台兼容性。
标签页的滑动动画、按钮的缩放动画、结果的弹出动画,这些细节让工具用起来更流畅。动画不是装饰,而是给用户反馈,让他们知道操作已经生效。
在实际项目中,Base64 常用于图片上传(转成 Data URL)、API 认证(Basic Auth)、数据传输等场景。理解了原理,遇到相关问题时就能快速定位和解决。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
更多推荐
所有评论(0)