我正在参加「掘金·启航计划」
在某天某时,我立下了这个奇怪的flag,关于我和他的爱恨情仇,请看我的第一篇文章
想要用RN写个小说APP第一天
当前已经实现的功能有:
- 爬取页面内容✅
- 状态管理以及数据持久化✅
- 小说内容主题自定义(字体/颜色/...)
- 爬取后如何进行文本分页✅
- 分页后如何在用户阅读时,无感加载上/下一章✅
项目地址,star看我后续更新🌠🌠🌠
今日内容
今天做的是小说搜索和详情的部分,使用了MMKV将搜索记录进行存储,使用new Function对编写的js代码进行还原得到搜索结果。
效果展示
思路
因为搜索页面和搜索结果页,都有着同样的搜索框组件。所以我并没有将其分为两个页面分开加入Stack.Navigator(之前第一章写过),在用户键盘的确认回调中,控制历史记录隐藏,搜索结果展示。
获取搜索记录并展示
在clickText中,需要将搜索结果设置为展示,并更新MMKV中的记录,在更新之前,对记录进行遍历,如果之前进行过一模一样的搜索记录时,将其移动到数组的最前面,保持规律。
// 代码并不完整
// 取到MMKV储存的值,第一次使用时,初始化成[]
const History = ()=>{
const [history = [], setHistory] = useMMKVObject<string[]>('user.history');
const clickText = (text: string) => {
console.log(text);
setSearxhText(text);
setIsShowHistory(false);
setHistory(
[text, ...history].filter((txt, index) => txt !== text || index === 0),
);
};
return (
<View style={Style.history_wrap}>
<Text style={Style.history_text}>
搜索历史:{' '}
<Text
onPress={() => setHistory([])}
style={[Style.history_text, {color: Style.history_list.color}]}>
清空
</Text>
</Text>
<View style={Style.history_content}>
{history.map((text, index) => (
<Text
key={index}
onPress={() => {
clickText(text);
}}
style={Style.history_list}>
{text}
</Text>
))}
</View>
</View>
)
}
获取搜索结果并展示
将搜索的文本传递给本组件,交由封装的自定义hook useSearchNovel去获取结果内容。结果需要遵循一定的格式。返回结果数组,这里使用的是RN自带组件FlatList进行渲染。
const SearchDetail: React.FC<{text: string}> = ({text}) => {
console.log('SearchDetail', text);
const DATA = useSearchNovel(text);
const navigation = useNavigation();
const renderItem = useCallback(({item: {title, url, cover, author}}) => {
return (
<TouchableOpacity
style={style.item}
onPress={() => {
navigation.navigate('NovelDetail', {
info: {title, url, cover, author},
});
}}>
<Image
style={style.cover}
source={{
uri: cover,
}}
/>
<View style={style.item_info}>
<Text style={style.title}>{title}</Text>
<Text style={style.author}>{author}</Text>
</View>
</TouchableOpacity>
);
}, []);
return (
<View style={style.warp}>
<FlatList
refreshing={DATA.length === 0}
data={DATA}
showsVerticalScrollIndicator={false}
renderItem={renderItem}
keyExtractor={item => item.url}
/>
</View>
);
};
自定义hook
现在来看看自定义useSearchNovel中如何得到我们的搜索结果:
const useSearchNovel = (text: string): any[] => {
const [data, setData] = useState([]);
useLayoutEffect(() => {
getListFromSource(text).then(res => {
setData(res);
});
}, [text]);
return data;
};
export default useSearchNovel;
很简单,主要逻辑其实是在getListFromSource中进行处理,需要从url中使用正则表达式替换成我们的关键词,这块的功能主要是得到请求下来的HTML文本,再继续交给getBookListWithRule进行解析:
export const getListFromSource = async (text: string) => {
const info = source[0];
const url = '/plus/search.php?kwtype=0&searchtype=搜索&q={{key}}'.replace(
/\{\{(.+?)\}\}/g,
str => {
return text;
},
);
const data = await fetch(info.bookSourceUrl + url).then(function (response) {
return response.text();
});
const $ = cheerio.load(data);
const res = getBookListWithRule($, info.bookSourceUrl);
return res;
};
getBookListWithRule
我暂定这一块是自定义实现的,传入cheerio生成的对象可以使用cheerio的所有方法,rule是这样的一个字符串:
const rule = `return $('.ul_b_list li')
.map(function (index) {
return {
title: $('h2 a', this).text(),
author: $('.state', this)
.text()
.match(/作者:(\\S*)/)[1],
cover: url+$('img', this).attr('src'),
url: url+$('h2 a', this).attr('href'),
};
})
.get();`
const getBookListWithRule = ($, url) => {
const ruleFn = new Function('$','url',rule);
return ruleFn($, url);
};
拿到的是结果是这样的一个数组:
[{
title:string,
author:string,
url:string,
cover:string
}]
小说详情页
const getBookDetailWithRule = ($, url) => {
const node = $('.Left');
const data = {
title: $('.title h1', node).text(),
author: $('.info li:nth-child(1) a', node).text(),
totalWords: $('#cms_ready_1', node).text(),
info: $('.words > p:nth-child(3)', node).text().substring(7),
cover: $('.pic img', node).attr('src'),
newer: $('.words a', node).text(),
tags: [$('.info .lb a', node).text()],
};
return data;
};
结束
- 搭建页面基础结构🎈
- 如何爬取页面内容💖
- 状态管理以及数据持久化🧡
- 小说内容主题自定义(字体/颜色/...)🤎
- 爬取后如何进行文本分页💘
- 分页后如何在用户阅读时,无感加载上/下一章💝