本文教你使用React Native复刻一个DeepSeek,目前没有完整复刻,只加入了核心的对话功能,你可以在此基础上继续开发,或者我还会继续更新
一、初始化项目
- 首先我们来到expo官网,按照这个步骤来初始化我们的项目
2. 我们这里起名为这个,
npx create-expo-app@latest deepseek-clone,接着我们等待下载完成,然后我们用Trae打开这个项目
- 我们先执行这个命令,重置一下整个项目
- 然后就可以expo启动了
- ok,现在是空项目状态,我们安装最好用的样式库,nativewind,来到官网
- 安装我们需要的库,
npx expo install nativewind tailwindcss@^3.4.17 react-native-reanimated@3.16.2 react-native-safe-area-context - 然后初始化tailwind配置文件
npx tailwindcss init
/** @type {import('tailwindcss').Config} */
module.exports = {
// NOTE: Update this to include the paths to all of your component files.
content: ["./app/**/*.{js,jsx,ts,tsx}", "./components/**/*.{js,jsx,ts,tsx}"],
presets: [require("nativewind/preset")],
theme: {
extend: {},
},
plugins: [],
};
- 接着我们一路按照官网的安装文件
- 我们在index.tsx里写点tailwind,发现已经可以了
二、搭建主页结构和样式
- 我们先把头部隐藏掉
import { Stack } from "expo-router";
export default function RootLayout() {
return <Stack screenOptions={{ headerShown: false }} />;
}
- 然后来到index.tsx首页我们可以把DeepSeek主页氛围三块,头部,中间区域,底部,以及对应的组件
- 主页中引入
import BuFooter from "@/components/bu/BuFooter/BuFooter";
import BuHeader from "@/components/bu/BuHeader/BuHeader";
import BuMain from "@/components/bu/BuMain/BuMain";
import { SafeAreaView } from "react-native-safe-area-context";
export default function Index() {
return (
<SafeAreaView className="flex-1">
<BuHeader />
<BuMain />
<BuFooter />
</SafeAreaView>
);
}
- 此时我们的app是这样的
- 我们接着给头部添加样式
import AntDesign from "@expo/vector-icons/AntDesign";
import Ionicons from "@expo/vector-icons/Ionicons";
import React from "react";
import { Text, TouchableOpacity, View } from "react-native";
function BuHeader() {
return (
<View className="flex-row justify-between items-center w-full px-6 py-4">
<TouchableOpacity>
<AntDesign name="bars" size={24} color="black" />
</TouchableOpacity>
<Text className="text-xl font-medium">新对话</Text>
<TouchableOpacity>
<Ionicons name="chatbubble-outline" size={24} color="black" />
</TouchableOpacity>
</View>
);
}
export default BuHeader;
- 主体样式结构
import React from "react";
import { Image, ScrollView, Text, TouchableOpacity, View } from "react-native";
export const mockMessages = [
{
id: 1,
role: "assistant",
content: "你好!我是DeepSeek assistant助手,很高兴为你服务。",
},
{ id: 2, role: "user", content: "你能帮我解释一下React Native是什么吗?" },
{
id: 3,
role: "assistant",
content:
"React Native是Facebook开发的一个开源框架,它允许开发者使用React和原生平台的功能来构建Android和iOS应用。它的特点是:\n\n1. 使用JavaScript和React编写代码\n2. 能够访问平台原生UI组件\n3. 热重载支持,开发效率高\n4. 一次编写,到处运行",
},
{
id: 4,
role: "user",
content: "听起来很不错!那它和普通的React开发有什么区别?",
},
{
id: 5,
role: "assistant",
content:
"主要区别在于:\n\n1. UI组件:React Native使用原生组件而不是网页组件\n2. 样式系统:使用类似CSS的JS对象,但不是所有CSS属性都支持\n3. 导航系统:使用专门的导航库,如React Navigation\n4. 平台特定代码:可以编写针对iOS和Android的特定代码",
},
{
id: 6,
role: "assistant",
content:
"主要区别在于:\n\n1. UI组件:React Native使用原生组件而不是网页组件\n2. 样式系统:使用类似CSS的JS对象,但不是所有CSS属性都支持\n3. 导航系统:使用专门的导航库,如React Navigation\n4. 平台特定代码:可以编写针对iOS和Android的特定代码",
},
{
id: 7,
role: "assistant",
content:
"主要区别在于:\n\n1. UI组件:React Native使用原生组件而不是网页组件\n2. 样式系统:使用类似CSS的JS对象,但不是所有CSS属性都支持\n3. 导航系统:使用专门的导航库,如React Navigation\n4. 平台特定代码:可以编写针对iOS和Android的特定代码",
},
];
const BuMain = () => {
return (
<ScrollView className="flex-1 px-4">
{mockMessages.map((message, index) => (
<View
key={index}
className={`flex-row ${
message.role === "user" ? "justify-end" : "justify-start"
} mb-4 items-end`}
>
{message.role === "assistant" && (
<Image
source={require("@/assets/images/ds.png")}
className="w-8 h-8 rounded-full mr-2"
/>
)}
<TouchableOpacity
className={`${
message.role === "user" ? "bg-blue-500" : "bg-gray-100"
} rounded-2xl px-4 py-3 max-w-[85%]`}
>
<Text
className={`text-base ${
message.role === "user" ? "text-white" : "text-gray-800"
}`}
>
{message.content}
</Text>
</TouchableOpacity>
</View>
))}
</ScrollView>
);
};
export default BuMain;
- 底部结构样式
import AntDesign from "@expo/vector-icons/AntDesign";
import React from "react";
import {
KeyboardAvoidingView,
Platform,
Text,
TextInput,
TouchableOpacity,
View,
} from "react-native";
import { SafeAreaView } from "react-native-safe-area-context";
const BuFooter = () => {
/**
* 你是谁,
* 你能做什么
*/
return (
<SafeAreaView className="w-full px-4 pt-1 h-[155px]">
<KeyboardAvoidingView
behavior={Platform.OS === "ios" ? "padding" : undefined}
>
<TextInput
placeholder="Message"
className="bg-gray-300/30 h-14 text-2xl rounded-3xl pl-5"
placeholderTextColor="#555"
returnKeyType="send"
/>
</KeyboardAvoidingView>
<View className="flex-row justify-between items-center p-4">
{/* 左 */}
<View className="flex-row gap-2">
<TouchableOpacity className="bg-blue-600/20 px-3 py-2 rounded-3xl">
<Text className="text-blue-500">深度思考</Text>
</TouchableOpacity>
<TouchableOpacity className="bg-blue-600/20 px-3 py-2 rounded-3xl">
<Text className="text-blue-500">联网搜索</Text>
</TouchableOpacity>
</View>
{/* 右 */}
<View className="flex-row gap-3 items-center">
<TouchableOpacity>
<AntDesign name="plus" size={23} color="gray" />
</TouchableOpacity>
<TouchableOpacity>
<AntDesign
className={`bg-blue-500 rounded-full p-2`}
name="arrowup"
size={20}
color="white"
/>
</TouchableOpacity>
</View>
</View>
</SafeAreaView>
);
};
export default BuFooter;
- ok,到这里我们已经搞完了整体结构样式了,并且模拟了一下对话数据,让我们来看一下成果
- 写样式的地方可以借助Trae的Builder模式,可以快速开发,然后不满意的部分可以自行调整
三、DeepSeek接口调用
- 我这里选择时硅基流动的接口,大家也可以根据自己的需求换其他厂商的,这里比较简单,直接上代码
interface Message {
role: 'user' | 'assistant';
content: string;
}
interface StreamResponse {
id: string;
model: string;
choices: {
delta: {
content?: string;
role?: string;
};
finish_reason: string | null;
index: number;
}[];
}
interface FetchStreamOptions {
onMessage?: (content: string) => void;
onError?: (error: Error) => void;
onFinish?: () => void;
}
export const baseURL = 'https://api.siliconflow.cn/v1/chat/completions'
export const headers = {
'Authorization': 'Bearer sk-tolrlhmjwnlx******',
'Content-Type': 'application/json',
}
export const body = {
model: 'deepseek-ai/DeepSeek-R1-Distill-Qwen-7B',
stream: false,
}
interface FetchDataOptions {
content?: string;
}
// 获取响应数据 , 非流式
export const fetchData = async ({content}: FetchDataOptions) => {
const res = await fetch(baseURL,{
method: 'POST',
headers,
body: JSON.stringify({
...body,
messages: [
{
role: 'user',
content
}
]
})
}).then(res => res.json())
console.log('fetchData',res.choices[0].message.content)
return res.choices[0].message.content
}
- 我们只要调用fetchData函数传入用户输入就可以获取到DeepSeek的回答了
四、状态逻辑
- 安装zustand作为全局状态管理,这里存储对话数据
import { create } from "zustand";
export type Message = {
id?: number;
role: 'assistant' | 'user';
content: string;
};
interface MessageStoreState {
messages: Message[];
setMessages: (messages: Message[]) => void;
}
export const useMessages = create<MessageStoreState>((set) => ({
messages: [],
setMessages: (messages) => set(() => ({ messages })),
}));
const useMessagesStore = () => {
return {
messages: useMessages((state) => state.messages),
setMessages: useMessages((state) => state.setMessages),
};
};
export default useMessagesStore;
- 接着我们就可以在BuMain组件进行渲染,当然这里可能是空的,我们先搞一个空状态组件
import React from "react";
import { Image, Text, View } from "react-native";
const BuEmpty = () => {
return (
<View className="w-full flex-1 justify-center items-center">
<View className="mx-auto w-4/5 flex items-center">
<Image
source={require("@/assets/images/ds.png")}
className="w-[70px] h-[70px]"
/>
<Text className="mt-6 text-3xl font-bold">HI,我是Bubble Seek</Text>
<Text className="mt-4 text-xl text-gray-600 text-center">
我可以帮你搜索、答疑、写作,请把你的任务交给我吧~
</Text>
</View>
</View>
);
};
export default BuEmpty;
- 接着我们在BuFooter组件中,获取用户输入,然后发送给DeepSeek获取响应再添加到全局状态里
4. 然后我们在输入框里绑定一下状态和事件处理函数
- 我们可以在发送按钮上优化一下体验,用户有输入了才能点击发送,没有输入内容则不能发送
6. 该组件整体代码如下
import { fetchData } from "@/apis";
import useMessagesStore, { useMessages } from "@/store/useMessageStore";
import AntDesign from "@expo/vector-icons/AntDesign";
import React, { useState } from "react";
import {
KeyboardAvoidingView,
Platform,
Text,
TextInput,
TouchableOpacity,
View,
} from "react-native";
import { SafeAreaView } from "react-native-safe-area-context";
const BuFooter = () => {
/**
* 你是谁,
* 你能做什么
*/
const [userInput, setUserInput] = useState("");
const { setMessages, messages } = useMessagesStore();
const handleSend = async () => {
console.log("userInput: ", userInput);
await setMessages([...messages, { role: "user", content: userInput }]);
setUserInput("");
const robotRes = await fetchData({ content: userInput });
console.log("handleSend: ", robotRes);
const currentMessages = useMessages.getState().messages;
await setMessages([
...currentMessages,
{ role: "assistant", content: robotRes },
]);
};
return (
<SafeAreaView className="w-full px-4 pt-1 h-[155px]">
<KeyboardAvoidingView
behavior={Platform.OS === "ios" ? "padding" : undefined}
>
<TextInput
placeholder="Message"
className="bg-gray-300/30 h-14 text-2xl rounded-3xl pl-5"
placeholderTextColor="#555"
returnKeyType="send"
value={userInput}
onChangeText={(text) => setUserInput(text)}
onSubmitEditing={handleSend}
/>
</KeyboardAvoidingView>
<View className="flex-row justify-between items-center p-4">
{/* 左 */}
<View className="flex-row gap-2">
<TouchableOpacity className="bg-blue-600/20 px-3 py-2 rounded-3xl">
<Text className="text-blue-500">深度思考</Text>
</TouchableOpacity>
<TouchableOpacity className="bg-blue-600/20 px-3 py-2 rounded-3xl">
<Text className="text-blue-500">联网搜索</Text>
</TouchableOpacity>
</View>
{/* 右 */}
<View className="flex-row gap-3 items-center">
<TouchableOpacity>
<AntDesign name="plus" size={23} color="gray" />
</TouchableOpacity>
<TouchableOpacity disabled={!userInput} onPress={handleSend}>
<AntDesign
className={`${
userInput ? "bg-blue-500" : "bg-blue-500/50"
} rounded-full p-2`}
name="arrowup"
size={20}
color="white"
/>
</TouchableOpacity>
</View>
</View>
</SafeAreaView>
);
};
export default BuFooter;
- 我们在主页对messages数组进行判断,若没有则为空状态,有则渲染一下主体
import BuEmpty from "@/components/bu/BuEmpty/BuEmpty";
import BuFooter from "@/components/bu/BuFooter/BuFooter";
import BuHeader from "@/components/bu/BuHeader/BuHeader";
import BuMain from "@/components/bu/BuMain/BuMain";
import useMessagesStore from "@/store/useMessageStore";
import { StatusBar } from "expo-status-bar";
import { SafeAreaView } from "react-native-safe-area-context";
export default function Index() {
const { messages } = useMessagesStore();
return (
<SafeAreaView className="flex-1 justify-between">
<BuHeader />
{messages.length > 0 ? <BuMain /> : <BuEmpty />}
<BuFooter />
<StatusBar style="auto" />
</SafeAreaView>
);
}
- 然后我们主体部分渲染的列表不用从假数据获取了,
9. 这里有个点,就是实现当消息列表过长时,DeepSeek回复完消息,ScrollView自动滚动到底部的功能
const scrollViewRef = useRef<any>(null);
useEffect(() => {
if (scrollViewRef.current) {
setTimeout(() => {
scrollViewRef.current.scrollToEnd();
}, 100);
}
}, [messages]);
- 整体代码如下,这里当消息数组变化时,ScrollView组件可能还未渲染结束,使用定时器延时一下再滚动到底部即可
import useMessagesStore from "@/store/useMessageStore";
import React, { useEffect, useRef } from "react";
import { Image, ScrollView, Text, TouchableOpacity, View } from "react-native";
const BuMain = () => {
const { messages } = useMessagesStore();
const scrollViewRef = useRef<any>(null);
useEffect(() => {
if (scrollViewRef.current) {
setTimeout(() => {
scrollViewRef.current.scrollToEnd();
}, 100);
}
}, [messages]);
return (
<ScrollView className="flex-1 px-4" ref={scrollViewRef}>
{messages.map((message, index) => (
<View
key={index}
className={`flex-row ${
message.role === "user" ? "justify-end" : "justify-start"
} mb-4 items-end`}
>
{message.role === "assistant" && (
<Image
source={require("@/assets/images/ds.png")}
className="w-8 h-8 rounded-full mr-2"
/>
)}
<TouchableOpacity
className={`${
message.role === "user" ? "bg-blue-500" : "bg-gray-100"
} rounded-2xl px-4 py-3 max-w-[85%]`}
>
<Text
className={`text-base ${
message.role === "user" ? "text-white" : "text-gray-800"
}`}
>
{message.content}
</Text>
</TouchableOpacity>
</View>
))}
</ScrollView>
);
};
export default BuMain;
11. 最后,去掉假数据,就完成了第一部分的核心功能