一、为什么选择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,但其
Collections、Documents等数据模型仍需要一点时间学习。 - 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设计 :
listDocuments、createDocument等方法命名清晰,配合Query条件筛选,让前端数据操作变得轻松自然。
边做Demo边写博客,断断续续终于写完了,耗时有点长,可能有错误的地方,欢迎指正。也欢迎大家来讨论~