大家好,这里是韶光日记开发团队,虽然叫团队,但是目前只有我一个人~
为什么决定做独立应用
决定做独立应用的开发是在23年春节后,当时一个人在出租屋,刷着手机,无意间小宇宙开始爆发,决定不再颓废下去,正好当时心情烦闷,于是决定搞个日记app来玩玩(经典开场!!)

技术选型
技术选型可选的并不是很多,首先原生应用开发并不熟悉,对于一个前端开发来说最合适的无非就是跨端开发:
- uniapp
- flutter
- react-native
uniapp 之前做过一个电商的小程序,并且适配了多端(小程序,h5,app),综合下来开发一些页面还可以,但是没办法谈性能和体验。
flutter 当时只是知道这个东西,大概看了一眼,学习成本也比较高,所以直接舍弃了
所以作为一个 react 死忠粉,我又盯上了 react-native,说起来大概在18年的时候,用react-native 做过一款外卖app,开发体验还算不错,但是由于当时对rn的性能优化以及对应用开发模式的不熟悉,导致做出来一坨翔。现在随着经验越来越丰富,决定还是把rn捞出来再试试。
准备工作
当时大概统计了一下,总共要解决以下问题,就可以做出来一个Demo
- 富文本编辑器
- 富文本渲染器
- 组件库
- 数据库
- 路由导航
- 本地缓存
- 图片存储
- 日历控件
- 长列表滚动
富文本编辑器
最初富文本编辑器是直接在 npm 上面找了个现成的,但是集成以后发现功能实在简单,并且想支持行内的图片显示必须得把图片放在服务器上,然后塞url进去,所以最初版本的图片是当做附件传入的,虽然勉强,但也不是不能用,先有了再改,主打一个程序员的自我修养。
后来由于实在功能不够用,基于 lexical 实现了一个webview的富文本编辑器

富文本渲染器
富文本编辑器的渲染采用了 react-native-render-html ,不得不说开发者是真的强,我可以直接把富文本丢进去,然后写一套公共的样式即可,总的来说跟平时在web上开发没什么太大的区别。
当然,还是有很多花活可以去整,比如说自定义一个标签:
<RenderHtml
enableExperimentalBRCollapsing
contentWidth={width || ScreenWidth}
source={{
html: newValue,
}}
renderers={{
img: (prop) => {
const imageData = getRenderImage(prop.tnode.init.domNode.attribs.uuid)
return (
<View style={{}}>
<FastImage
resizeMode='contain'
style={{
width: '100%',
// height: imageData.height,
borderRadius: 12,
aspectRatio: imageData.width / imageData.height,
}}
source={{
uri: imageData.uri,
}}
/>
</View>
)
},
}}
enableExperimentalMarginCollapsing={true}
backgroundColor={Colors.transparent}
/>

组件库
其实当时对我来说,做一款app简单并不清楚上线等问题,完全就是一股脑的冲动,甚至连ui都没有,但是对于一个经验丰富的程序员来说,有个组件库,可以解决一切问题。
组件库考虑了很多,比如 react-native-elements react-native-paper 等等,最终选择了 react-native-ui-lib,原因就是它不仅好看,还有成型的demo
并且组件的数量可比较可观,还有丰富的颜色管理系统和静态资源管理系统,更是大大缩短了项目启动的时间。
当然最喜欢的还是下面这种写法~
function App() {
return (
<View paddingH-30>
<Text color={Colors.textTitle} text60 marginB-10>
{t('故事')}
</Text>
<Text color={Colors.textTitle} text80 marginB-20>
{t('在纷繁的世界中,我们渴望将生活中的珍贵瞬间封存在心底,让它们永不褪色。')}
</Text>
</View>
)
}数据库
当时不想搞服务器端,所以摆在我面前的选择就剩本地数据库可选了:
- Realm
- react-native-sqlite-storage
由于自己对数据库的不熟悉,以及懒得写sql,所以本人义无反顾的选择了 Realm ,这也是后面噩梦的开始,当然这个都是后话了。
Realm 当然是有自己的独到之处的,首先对rn有很好的支持,并且可以通过hook跟react有很好的集成,但是绑定的太深我不是很喜欢,所以我还是采用了比较保守的方式做了集成,通过实现了一套获取数据的api来实现数据的增删改查。
import Realm from 'realm';
import {
Diary,
deleteRealm,
writeToRealm,
queryRowFromRealm,
queryAllFromRealm,
clearAllFromRealm,
clearRowFromRealm,
} from './index';
export const name = 'Colors';
export const schema = {
name,
primaryKey: 'id',
properties: {
id: 'string',
label: 'string',
color: 'string',
delete: {type: 'bool', default: false},
createTime: {type: 'date', default: new Date()},
},
};
// 写入数据
export function write(obj) {
return writeToRealm(obj, name);
}
export function deleteDiary(obj) {
return deleteRealm(obj, name);
}
// 查询指定数据
export function queryRow(id) {
return queryRowFromRealm(id, name);
}
// 查询数据
export function queryAll(query = '', start, end) {
return queryAllFromRealm(name, `${query}delete == false`, start, end);
}
// 查询日记数据
export function queryHandAll(query = '', start, end) {
return queryAllFromRealm(name, `${query}delete == false`, start, end);
}
// 清除数据
export function clearAll() {
return clearAllFromRealm(name);
}
// 删除行
export function clearRow(id) {
return clearRowFromRealm(id, name);
}
路由导航
路由前期选择了@react-navigation/native-stack 但是在安卓的动画实在没找到怎么改,所以后续老老实实改回了 @react-navigation/stack算是踩了最大的一个坑。
不过本人实在喜欢剑走偏锋, 所以只是用了本身的路由功能,导航头什么的都自己写了,省的研究api,以至于走火入魔。
<Stack.Screen
name="Info"
options={{title: t('故事详情'), headerShown: false}}
component={Info}
/><Layout ref={layoutRef}>
<ActionBar rights={rights} actions={action} />
<FlashList
nestedScrollEnabled
ref={scrollRef}
enableDynamicSizing
showsHorizontalScrollIndicator={false}
showsVerticalScrollIndicator={false}
ListHeaderComponent={header}
estimatedItemSize={300}
data={data.data}
renderItem={renderContent}
keyExtractor={item => item?.id || Date.now()}
ListEmptyComponent={empty}
ListFooterComponent={footer}
onScroll={event => {
offsetY.value = event.nativeEvent.contentOffset.y;
setShowTitle(event.nativeEvent.contentOffset.y > 50);
}}
extraData={[batchEdit, batchEditList, data]}
onEndReachedThreshold={0.5}
onEndReached={onEndReached}
/>
</Layout>
本地缓存
本地存储不同于数据库,准确来讲相当于app端的 localStorage 用来存储一些状态,这个没什么好讲的,无脑上了 @react-native-async-storage/async-storage 。
当然代价还是有的,前期获取配置信息都是通过 redux 同步去取数据,导致启动时间过长,后来发现 AsyncStorage.multiGet 可以一次性获取到想要的配置,大大缩短了启动时间。
另外在后续实现日记的草稿功能时,无意间在安卓手机上发现塞的内容太多,会导致内存爆掉,所以还是要控制一下写入的数据大小,尽量少存一些东西。
图片存储
本地存储图片依赖了 react-native-fs @react-native-camera-roll/camera-roll 一个用来存储照片,一个用来从相册或者相机获取照片,并且提供了压缩或者转base64的功能。
在新的富文本编辑器中,还通过读取图片的base64编码注入到编辑器,实现了本地图片插入富文本的功能。
日历控件
日历控件找了很多,最终用了 react-native-calendars ,只不过已经停更很久了,并且存在一些bug,拉下来源码魔改了一下,实现了现在的功能,

长列表滚动
长列表的滚动在任何情况下都有的性能问题,尤其是在移动端,对于交互和性能有很高的要求,rn本身提供了一些高性能列表的优化方案,但是奈何本人实在是想直接把富文本摊到列表页,导致性能问题更加严重,经过多方尝试,最终采用了 flash-list 才解决掉这个问题。

总结
这个就是我的第一款独立应用,主要是做了一些技术上的大胆尝试,目前运营情况说不上好,也有一些用户在长期使用,希望能越做越好。
韶光日记开发团队,祝好!
