【React Native】如何在开发中使用Appwrite

一、为什么选择Appwrite

最近在做React native的项目,研究后端的时候考虑了几种方法,先是按照以往的方法自己搭了node+mysql的数据库,但是实际使用的时候发现很麻烦:

  • 一方面是使用了expo框架,每次启动项目的时候,expo和node服务器都需要单独跑,开发流程变得很复杂。
  • 一方面是在手机端预览的时候环境非常不稳定 ,各种数据的更新都不及时,降低了开发效率。由于 Expo 运行在手机中,而本地 Node + MySQL 跑在电脑上,手机并不能直接访问 localhost,这导致开发过程中每次请求数据非常不稳定。

参考了其他的项目,最后选用了appwrite,它把数据库、用户、权限、日志,做成了可视化控制台。我们只需要在网页上 就可以在控制台中创建数据表、自动生成测试数据、开关权限和查看日志,集合了很多我们所需的功能,并且是开源的,对于做demo而言十分便捷。

二、Appwrite介绍

关于Appwrite,可以这样介绍:Appwrite 是一个开箱即用的后端服务平台,将后端复杂的功能模块化,让开发者不用手写后端代码,在网页上就可以实现账号、数据库、云函数、开发日志等功能。

  • 它是开源的,可以自己部署,也可以直接用官方的 Cloud 服务。
  • 支持 Web / iOS / Android / Flutter / React Native / Next.js / Vue 等前端框架。

Appwrite 主要功能模块:

模块 作用
Auth 登录注册、OAuth、JWT 管理
Database 类似文档数据库,支持权限控制
Storage 图片/文件存储、访问权限
Functions 云函数,可以跑 server 逻辑
Realtime 数据实时监听(聊天室、点赞数实时更新)

三、优缺点

优点

  • 界面简洁美观,控制台UI舒适,操作简洁易懂

  • 权限模型直接可视化,对于数据中的每个表的权限,创建、读、改都可以直接GUI调整。Appwrite用UI把权限(owner / user / role / team / public)做成了可视化界面。

  • API简洁直观:在程序中使用API向appwrite中发起请求的时候,使用的API十分简洁易懂,好上手

    • listDocuments() → 获取文档列表
    • createDocument() → 创建文档
    • updateDocument() → 更新文档

缺点

  • 相关的资料不多,开发过程中会遇到的问题有查不到的可能,需要自己摸索。比如我在开发过程中遇到Appwrite的更新,新版本中引入了table的概念,但是在先前的一些文档中介绍的都是attribute,让我研究了好一会哈哈哈。
  • 对于相关概念需要一定理解:Appwrite不是单纯的数据库,虽然类似 Firebase,但其CollectionsDocuments 等数据模型仍需要一点时间学习。
  • Appwrite的字段是强结构化的,创建后不能修改字段名。表的结构一定要设计好,否则后期维护会很困难。

四、如何使用

1. 创建 Appwrite 项目

  • 在Appwrite网页中打开console,创建一个新项目,设置好平台
  • 你可以先进行第二点中提到的配置,也可以先创建数据库和表。
  • PS:如果你看的是先前的教程,在创建完数据库后他们会开始创建 'Collection',在曾经版本的Appwrite中,在Database下面是'Collection',然后在Collection中创建'Attribute'作为数据。Collection会有ID(是一串字母+数字的格式),需要写在文件里,但是新版本取消了'Collection'的概念,更换为'Table',更贴近于数据库的逻辑,方便你创建数据。

2.接下来按照网站给出的顺序进行项目配置

  • 在编译器中打开终端,安装Appwrite
bash 复制代码
git clone https://github.com/appwrite/starter-for-react-native
cd starter-for-react-native
  • 在.env文件中添加你的Appwrite相关ID(项目ID、数据库ID、表ID......),这是连接客户端的钥匙

注意:在先前版本中需要写上每一个'Collection'的ID(都是数字+字母的形式),有些复杂且不好辨别,现在你只需要写上你的数据库ID,剩下的表的ID都和表名相同,大大方便了我们。更新后,你甚至可以不用提前声明表的ID,而是在使用时直接灵活运用。

  • 不要忘记调整你的表的使用权限,否则请求的时候会因为没有权限而请求失败

3. 在appwrite.ts中初始化 Client

ini 复制代码
import { Client, Databases } from 'appwrite';
​
const DATABASE_ID = process.env.EXPO_PUBLIC_APPWRITE_DATABASE_ID!;
const ENDPOINT = process.env.EXPO_PUBLIC_APPWRITE_ENDPOINT!;
const USERS_TABLE_ID = process.env.EXPO_PUBLIC_APPWRITE_USERS_TABLE_ID!;
const NOTES_TABLE_ID = process.env.EXPO_PUBLIC_APPWRITE_NOTES_TABLE_ID!;
​
export const client = new Client()
    .setEndpoint(ENDPOINT) // https://cloud.appwrite.io/v1
    .setProject(PROJECT_ID);
​
export const databases = new Databases(client);

4. 读写数据库

想要从数据库中获取数据,就需要在fetch方法中使用listDocuments()。

listDocuments() 的作用就是:从某个 collection 里把数据拉出来。传入的两个参数分别是你在Appwrite中的Database的ID和 Table的ID,默认获取到表中的全部数据。

想要做精细化的筛选,可以使用Query来进行条件选择。

less 复制代码
const res = await databases.listDocuments(
  "databaseId",
  "tableId",
  [
    Query.equal("userId", currentUserId),
    Query.orderDesc("$createdAt"),
    Query.limit(20),
  ]
);

这段等价于SQL中的'WHERE userId = xxx ORDER BY createdAt DESC LIMIT 20'。

条件筛选

Query的常用方法有:

  • Query.equal('userId', '123')等价于userId == '123'(可以用于获取某些类型的数据,或是某些用户的数据)
  • Query.between('age', 18, 30)等价于age between 18 and 30 (inclusive)
  • Query.contains('tags', 'holiday') 等价于array/string contains 'holiday'
  • Query.orderDesc('$createdAt') 是按创建时间降序
  • Query.orderAsc('price') 是按 price 升序
  • Query.limit(20)每页 20 条
  • Query.offset(40) 跳过前 40 条(页码式分页)

返回结果

listDocuments 这个方法返回的结构里通常包含 total(符合条件的总条数)和 documents(当前页数组)。如果数据量很大,想要做无限滚动的页面,需要使用分页机制,可以参考我的另一篇文章。 【React Native+Appwrite】获取数据时的分页机制

使用例子:

javascript 复制代码
const res = await databases.listDocuments('dbId', 'collectionId');
console.log(res.documents);
​
export async function getUsers(page = 1, limit = 15) {
    const offset = (page - 1) * limit;
​
    try {
​
        const result = await databases.listDocuments(DATABASE_ID, USERS_TABLE_ID, [
            Query.limit(limit),
            Query.offset(offset),
            Query.equal("sex", "male"),
        ]);
        //等价于跳过offset个数据,找到limit个'sex'字段等于'male'的数据
​
​
        console.log('成功连接!')
        console.log('获取到的用户数据:', result)
​
        return result
    } catch (error) {
        console.error('连接 Appwrite 失败:', error)
    }
}
​

5. ReactNative 中一些容易报错的点

Localhost Endpoint问题:

我们在配置客户端的时候,需要配置Endpoint和Project,如果写下http://localhost/v1作为Endpoint,后续在手机端上运行项目的时候可能会出现请求失败的错误,这是因为项目中的localhost指的是运行该进程的设备本身 。所以在手机上的http://localhost/v1会去尝试手机本机的端口,而不是电脑上的Appwrite服务器,会导致网络请求失败。

那么如何解决这个问题呢?

Appwrite Cloud :把Endpoint设置成https://cloud.appwrite.io/v1,操作简单、适合Expo使用。

  • 这是Appwrite 官方托管的云平台,官方帮你托管服务、数据库、文件等。
  • 无需搭建后端或服务器
  • 手机端天然可访问
  • 稳定、最接近生产环境,免去了 IP 变化的烦恼

可以理解为 Appwrite 的 "Firebase 模式" :所有服务都在云端,由官方维护。

那么在上文中Appwrite官方给出的Endpoint是什么呢?是https://fra.cloud.appwrite.io/v1,是由于我在创建项目时选择了德国法兰克福区域,系统自动为我加入了法兰克福的地域标识代码fra,不加的话就是全局的入口。

总结:

  • https://cloud.appwrite.io/v1全球入口
  • https://fra.cloud.appwrite.io/v1区域入口
  • Cloud 会自动把你的项目分配到某个区域。

使用局域网IP :如果你坚持自己搭 Appwrite(本地或内网服务器),可以用你电脑的局域网 IP 替代 localhost,局域网IP可以在 Windows 命令行输入 ipconfig查看(通常是 192.168.x.x)。然后把Endpoint设置成http://192.168.x.x/v1

  • 使用的前提是手机和电脑在同一个WiFi下
  • Appwrite 已允许来自局域网的访问

内网穿透:如果你想让别人访问你的本地Appwrite,可以使用ngrok、localtunnel、Cloudflare Tunnel进行内网穿透。把你本地的某个端口暴露成一个公网地址,这样外网设备(其他人的设备)就能访问你的本地服务,不需要在路由器上做端口映射或改 DNS。

命令示例:

复制代码
ngrok http 80

会生成一个公网 URL,如:

arduino 复制代码
https://abc123.ngrok.io

然后你就可以这样写:

vbscript 复制代码
.setEndpoint("https://abc123.ngrok.io/v1")

适合展示 demo、临时给同事使用。

Expo 开发服务器的 HTTPS 要求

在 Expo Go 应用中,如果你的 Appwrite Endpoint使用的是自签证书的 HTTPS 或纯 HTTP,可能会被阻止,导致 Network request failed 错误。这是因为Expo Go 为了安全,对非加密或证书不受信任的 HTTP 请求有严格限制。

解决方法

  • 使用 Appwrite Cloud:这是最简单的方法,因为它提供了有效的 HTTPS 证书。
  • 使用 expo start --tunnel(通过隧道生成一个可被手机访问的 https 地址)
  • 使用 ngrok http 80,把 http://localhost:80 暴露成 https://xxxx.ngrok.io,然后把 Appwrite endpoint 设为 https://xxxx.ngrok.io/v1
RN 没有 Browser API

React Native 运行在 JavaScriptCore 或 Hermes 引擎中,没有 DOM 和 BOM,所以许多的API都没有:

  • 没有window、document对象
  • 没有localStorage、sessionStorage
  • 没有Cookie API
  • 没有XMLHttpRequest(但有兼容实现)
  • 没有FormData的完整实现

五、实战Demo

在实战中,我的写法是:(前提是已经配置好.env文件)

javascript 复制代码
//appwrite.ts
import { Client, Databases, Query } from "react-native-appwrite"
​
const DATABASE_ID = process.env.EXPO_PUBLIC_APPWRITE_DATABASE_ID!;
const TABLE_ID = process.env.EXPO_PUBLIC_APPWRITE_TABLE_ID!;
const NOTES_TABLE_ID = process.env.EXPO_PUBLIC_APPWRITE_NOTES_TABLE_ID!;
​
​
//初始化客户端
const client = new Client().setEndpoint('https://cloud.appwrite.io/v1').setProject(process.env.EXPO_PUBLIC_APPWRITE_PROJECT_ID!)
​
const databases = new Databases(client);
​
//获取数据
export async function getNotes(page = 1, limit = 15) {
    
    //偏移量
    const offset = (page - 1) * limit;
​
    try {
​
        //偏移offset条,获取limit个'type==plan'的数据
        const result = await databases.listDocuments(DATABASE_ID, NOTES_TABLE_ID, [
            Query.limit(limit),
            Query.offset(offset),
            Query.equal("type", "plan"),
        ]);
​
        //获取完后输出一下结果
        console.log('成功连接!')
        console.log('获取到的用户数据:', result)
​
        return result
    } catch (error) {
        console.error('连接 Appwrite 失败:', error)
    }
}
ini 复制代码
//index.tsx
​
const Plan = () => {
    const router = useRouter();
​
    const [refreshing, setRefreshing] = useState(false);
    const [loading, setLoading] = useState(false);
    const [notes, setNotes] = useState<any[]>([]);
    const [page, setPage] = useState(1); // 分页页码
    const [hasMore, setHasMore] = useState(true); // 是否还有更多数据
​
    // 加载数据
    const loadNotes = useCallback(
        //reset用于记录是否重新加载
        async (reset = false) => {
            if (loading) return; // 避免重复请求
            setLoading(true);
​
            try {
​
                //计算请求的页码,如果是重新加载,那么从第1页开始加载,如果不是,从第page页开始
                const currentPage = reset ? 1 : page;
​
                const result = await getNotes(currentPage); //获取数据
                const data = result.documents;
​
                if (reset) {
                    //如果要重置(遇到了下拉刷新的情况),那么重新设置data和page
                    setNotes(data);
                    setPage(2); // 下一页页码
                } else {
                    //如果不是重置(上拉加载新页面),设置好Notes的数据,根据 '$id'筛选掉重复的数据,然后追加到notes里
                    setNotes(prev => [...prev, ...data.filter(d => !prev.some(p => p.$id === d.$id))]);
                    //设置页数
                    setPage(prev => prev + 1);
                }
​
                if (!data || data.length < 15) setHasMore(false);
            } catch (err) {
                console.error(err);
            } finally {
                setLoading(false);
            }
        },
        [loading, page]
    );
​
    useEffect(() => {
        loadNotes();
    }, []); // 初次加载
​
    useEffect(() => {
        console.log("notes更新:", notes);
    }, [notes]); // 每次 notes 变化时打印
​
​
    // 下拉刷新
    const onRefresh = async () => {
        setRefreshing(true);
        await loadNotes(true);
        setHasMore(true);
        setRefreshing(false);
    };
​
    // 上拉加载更多
    const onEndReached = async () => {
        if (!loading && hasMore) {
            await loadNotes(false);
        }
    };
​
    return (
        <View style={styles.container}>
​
            <ScrollView showsVerticalScrollIndicator={false}>
​
            {/* 笔记列表,用FlatList来加载,NoteCard组件可以自由发挥 */}
            <FlatList 
                data={notes}
                numColumns={2}
                contentContainerStyle={{
                paddingHorizontal: 16,
                paddingBottom: 60,
                }}
                columnWrapperStyle={{justifyContent: "space-around",}}
                keyExtractor={(item) => item.$id}
                renderItem={({ item }) => (
                <NoteCard note={item} />)}
            />
​
            </ScrollView>
​
        </View>
    );
};
​
​
export default Plan;
​

运行后控制台会输出结果:

六、总结

如果你正在开发一款基于ReactNative+Expo的App,甚至只是一个Demo,想减少后端工作量,实现快速、轻量化的开发,那么Appwrite是一个非常值得尝试的BaaS平台。

它的优点在于:

  • 它是开源平台,可自行部署
  • 提供了简洁的可视化后台,极大地方便了操作
  • API设计与文档清晰完善
  • 支持多端:Web、React Native、Flutter、iOS、Android
  • 内置 数据库、用户认证、文件存储、函数云、推送等完整后端能力
  • 具有直观的API设计listDocumentscreateDocument等方法命名清晰,配合Query条件筛选,让前端数据操作变得轻松自然。

边做Demo边写博客,断断续续终于写完了,耗时有点长,可能有错误的地方,欢迎指正。也欢迎大家来讨论~

相关推荐
GISer_Jing7 小时前
跨平台Hybrid App开发实战指南
android·flutter·react native
努力学前端Hang1 天前
移动端跨平台开发深度解析:UniApp、Taro、Flutter 与 React Native 对比
前端·javascript·react native·react.js
浪遏2 天前
好久不见 ,甚是想念 | vibe coding 一个react native 全栈项目| 小账兜
react native·全栈·vibecoding
GISer_Jing3 天前
跨端框架对决:React Native vs Flutter深度对比
flutter·react native·react.js
探索宇宙真理.3 天前
React Native Community CLI命令执行 | CVE-2025-11953 复现&研究
javascript·经验分享·react native·react.js·安全漏洞
Cxiaomu5 天前
React Native App 自动检测版本更新完整实现指南
javascript·react native·react.js
光影少年5 天前
React Native第六章
javascript·react native·react.js
千里马-horse5 天前
搭建 React Native 库
javascript·react native·react.js·native library
Cxiaomu6 天前
React Native App 图表绘制完整实现指南
javascript·react native·react.js