Docs 菜单
Docs 主页
/ /
Atlas App Services
/ /

教程:将React Native应用程序迁移到 PowerSync

在此页面上

  • 1 阶段:准备和设置
  • 部署Atlas集群
  • 将 PowerSync IP 添加到IP访问列表
  • 导入示例数据
  • 设置 PowerSync
  • 查看同步数据
  • 阶段 2:准备用于迁移的Realm应用
  • 克隆初始项目
  • 将Device Sync项目重构为仅本地
  • 运行并验证更改
  • 3 阶段:从Realm迁移到 PowerSync 客户端
  • 安装依赖项
  • 迁移数据模式
  • 重构应用程序代码
  • 运行并验证更改
  • 阶段 4:将数据从Atlas同步到 PowerSync
  • 初始设置
  • 重构客户端代码
  • 运行并验证更改
  • 5 阶段:实施后端API
  • 检查连接器
  • 实施上传方法
  • 创建后端服务器
  • 运行并验证更改
  • 阶段 6:最后的处理和清理
  • 实施“显示全部”切换(可选)
  • 实施离线模式切换(可选)
  • 清理项目
  • 结论和后续步骤

自 9 月 2024起, Atlas Device SDK (Realm)、 Device Sync和App Services已被弃用。 这意味着这些服务的用户必须在 9 月 2025之前迁移到另一个解决方案。 如果需要更多时间,请联系支持。

PowerSync 是Atlas Device Sync的首选替代方案。 它是基于 SQLite 的解决方案,如果您有使用Device Sync 的移动应用程序,则可能是正确的迁移解决方案。

本教程将指南您完成将以React Native编写的Device Sync移动应用程序迁移到 PowerSync 所需的步骤。 后端数据将保留在Atlas中,因此您需要配置 PowerSync 服务,更新本地数据库模式和绑定,并设立用于写入Atlas 的后端服务。

本教程使用适用于React Native的Realm待办事项列表应用程序,该应用程序可在 2域 powersync示例存储库中找到。

首先,您需要部署Atlas集群并输入一些测试数据。 这将指南您,就像您第一次设置Atlas一样。 如果您已经部署了集群,请随意跳过。

  1. 导航到MongoDB Atlas并注册Atlas帐户,如果您已经有帐户,请登录。

  2. 接下来,创建一个集群。

    用户界面的屏幕截图

    出于测试目的,请选择具有默认设置的 M0(免费)集群。 请随意进行任何其他更改以满足您的需求。

  3. 单击创建部署。

    用户界面的屏幕截图

    您将返回到仪表盘。 将自动显示“连接到集群”模式。

  4. 单击“选择连接方法”,然后选择“驱动程序”。

    用户界面的屏幕截图

    在此屏幕中,复制步骤 3 中显示的URL 。

    用户界面的屏幕截图

    将连接字符串添加到应用程序代码中。这是您的连接字符串;它是访问权限MongoDB实例所必需的。 保存连接字符串以供将来参考。

    您将在接下来的步骤中创建用户名和密码,PowerSync实例将使用它们连接到数据库。

  5. 单击 Done(完成)关闭模态窗口。

    集群完成部署后,仪表盘应如下所示。

  6. 单击“添加数据”以创建新数据库。

    用户界面的屏幕截图

    Create a Database on Atlas卡片上,单击 START

    用户界面的屏幕截图

    创建名为 PowerSync 的数据库和名为 Item 的集合,然后单击 Create Database

    用户界面的屏幕截图

    您将返回仪表盘,并应看到新创建的数据库和集合:

    用户界面的屏幕截图

    最后,您需要创建一个 PowerSync 用于连接到此数据库的新用户。

    在左侧边栏中,单击“安全”标题下的“数据库访问”。

    用户界面的屏幕截图

    单击“添加新数据库用户”,创建一个名为powersync 的新用户并提供密码。请注意要在之前复制的连接字符串中使用的用户名和密码。

    注意

    如果用户名或密码包含以下任何特殊字符,则必须将其转换为连接字符串的URL安全格式:$:/?!#[]@。 您可以手动执行此操作,也可以使用URL编码应用程序,例如 urlencoder.org。

    在“数据库用户权限”部分,单击“添加特定权限”,为 readWritedbAdminPowerSync数据库的 角色和 角色添加权限。

    用户界面的屏幕截图

    单击“添加用户”。

    您应该会看到新创建的用户具有所需的数据库权限。

    用户界面的屏幕截图

有关用户权限的更多详细信息,请参阅 PowerSync 源数据库设置指南的MongoDB部分。

为了使 PowerSync 能够访问权限Atlas中运行的数据库,您必须将服务IP地址添加到IP访问列表中。 PowerSync 安全和IP筛选文档中列出了这些IP地址。

在左侧边栏中,单击“安全”标题下的“网络访问”。

单击“+ 添加IP地址”,然后输入IP解决。为了更好地帮助将来管理此列表的人员,我们还建议输入 PowerSync 作为可选注释。

单击“确认”,然后对每个IP重复上述步骤。

如果还没有,请使用数据库用户的用户名和密码更新之前复制的连接字符串中的占位符。

在此步骤中,您将导入一些示例数据,这些数据将用于在后续步骤中同步数据。

首先,安装MongoDB Database Tools以访问权限mongoimport请参阅适用于您的操作系统的安装指南说明。

安装 database-tools 后,在终端中输入以下内容,确认您可以访问权限mongoimport

mongoimport --version

这应返回工具的版本。 如果遇到问题,请参阅上述安装指南。

接下来,创建一个名为 sample.json 的JSON文件,其中包含以下内容:

[
{
"isComplete": false,
"summary": "Complete project documentation",
"owner_id": "mockUserId"
},
{
"isComplete": true,
"summary": "Buy groceries",
"owner_id": "mockUserId"
},
{
"isComplete": false,
"summary": "Schedule dentist appointment",
"owner_id": "mockUserId"
},
{
"isComplete": false,
"summary": "Prepare presentation for next week",
"owner_id": "mockUserId"
},
{
"isComplete": true,
"summary": "Pay utility bills",
"owner_id": "mockUserId"
},
{
"isComplete": false,
"summary": "Fix bug in login system",
"owner_id": "mockUserId2"
},
{
"isComplete": false,
"summary": "Call mom",
"owner_id": "mockUserId"
},
{
"isComplete": true,
"summary": "Submit expense reports",
"owner_id": "mockUserId2"
},
{
"isComplete": false,
"summary": "Plan team building event",
"owner_id": "mockUserId2"
},
{
"isComplete": false,
"summary": "Review pull requests",
"owner_id": "mockUserId2"
}
]

此示例数据包含一些待办事项列表项。 本教程后面将使用 owner_id 来筛选示例。

要导入此JSON,请输入以下命令,用连接字符串替换 <connection-string> 占位符:

mongoimport --uri="<connection-string>" --db=PowerSync --collection=Item
--file=sample.json --jsonArray

您应该会看到以下消息:

10 document(s) imported successfully. 0 document(s) failed to import.

如果没有,请确认命令参数(包括连接字符串)是否正确,以及您的Atlas 用户是否具有正确的数据库访问权限。

您可以通过导航到Atlas用户界面中的集合或使用MongoDB Compass可视化桌面应用程序来查看和管理插入的文档。 要通过MongoDB Compass查看和管理数据库和集合,必须使用相同的连接字符串进行连接。

用户界面的屏幕截图

现在导航到 PowerSync 并注册或登录。

如果您是首次登录,则需要创建一个新实例才能开始。

创建一个名为 TodoList 的新实例。

用户界面的屏幕截图

选择MongoDB作为连接数据库。

用户界面的屏幕截图

使用Atlas连接字符串填充连接设置。

重要

使用不包含用户名、密码或其他URL参数的缩短版本的连接字符串。 示例,您的连接将显示为 mongodb+srv://m0cluster.h6folge.mongodb.net/

输入您在上一步中分配给该帐户的数据库名称(“PowerSync”)、用户名(“PowerSync”)和密码。

用户界面的屏幕截图

单击测试连接以确保可以成功连接。

如果您看到以下错误,请确认所需的所有 PowerSync 服务 IP 都在您的Atlas IP访问列表中。

用户界面的屏幕截图

如果问题仍然存在,请参阅《关于MongoDB连接的 PowerSync 数据库连接指南》。

单击“下一步”部署新的 PowerSync实例。这可能需要几分钟才能完成。

部署实例后,您可以通过创建一些基本同步规则来确保可以查看迁移的数据。

首先,删除默认同步规则并将其替换为以下规则:

bucket_definitions:
user_buckets:
parameters: SELECT request.user_id() as user_id
data:
- SELECT _id as id, * FROM "Item" WHERE bucket.user_id = 'global'
OR owner_id = bucket.user_id

为了使项目正确同步到 PowerSync 服务,请注意以下事项:

  • _id 必须映射到 id

  • 集合名称“Item”必须用引号括起来。 这是因为我们的集合名称以大写字母开头。

  • 用户特定的存储桶必须与 globaluser_id 匹配,从而提供对整个数据库的访问权限。 否则,您将匹配提供的 user_id,该 将从身份验证令牌中检索。

请注意,PowerSync 同步规则是一个相当深入的主题。 要学习;了解更多信息,您可以查看此同步规则博文或 PowerSync 同步规则文档。

单击 Save and Deploy(保存并部署)。同样,部署可能需要几分钟才能完成。

部署完成后,您应该会看到以下内容:

用户界面的屏幕截图

部署完成后,您应该会看到相应的状态。

如果出现任何错误,请确保为PowerSync 用户设立了 PowerSync 源数据库设置文档中列出的权限。

单击管理实例以查看同步规则和部署状态。

要完成此设置,您将使用 PowerSync 诊断应用程序查看刚刚创建并添加到同步规则中的待办事项列表项。 要使用此工具,首先需要创建开发令牌。

  • 在 PowerSync 页面顶部,单击 Manage Instances

  • 在左侧边栏中,单击 TodoList 旁边的省略号 (…) 打开此实例的上下文菜单,然后选择编辑实例

  • 选择客户端身份验证标签页,然后单击启用开发令牌

  • 单击保存并部署

用户界面的屏幕截图

单击 TodoList 旁边的省略号 (...) 再次打开该实例的上下文菜单,然后选择 生成开发令牌

系统会要求您提供令牌 subject/user_id。 这将充当 user_id,您可以设立同步规则以对其进行操作。

使用我们之前定义的同步规则,您可以将 subject/user_id设立为 global,以生成有权访问权限整个数据集的令牌。 您也可以设立为 mockUserIdmockUserId2 以在特定的 owner_id 上进行同步。

复制生成的令牌,然后打开诊断应用并粘贴开发令牌。

注意

开发令牌将在 12 小时后过期。 诊断工具在到期后将停止与Atlas同步,因此如果您希望其恢复同步,您必须生成新令牌。

您应该会看到与此类似的页面。

用户界面的屏幕截图

在左侧边栏中,单击SQL Console

创建 SELECT查询以查看所有项目:

SELECT * FROM Item
用户界面的屏幕截图

您现在拥有将MongoDB 数据库同步到移动应用程序所需的所有服务。

在此阶段,您将为React Native克隆一个Realm待办事项列表应用程序。 main示例存储库的 分支包含迁移的最终结果。

要使用示例存储库跟随本指南进行操作,请查看 00-Start-Here 分支:

git clone https://github.com/takameyer/realm2powersync
cd realm2powersync
git checkout 00-Start-Here

接下来,安装依赖项,以便编辑器可以接受任何导入,并确保在编辑此项目时没有错误。

重要

本教程假定您已安装最新版本的 Node.js。

npm install

由于该应用程序假定存在一个具有活动Device Sync服务的Atlas 集群,因此它尚不可运行。 在接下来的步骤中,您将进行所需的修改,以将该项目作为仅限本地的应用程序运行。

您必须删除Atlas Device Sync部件,应用程序才能使用仅本地数据运行。

首先,打开 source/AppWrapper.txs,然后删除AppProviderUserProvidersync 配置。

更新后的 AppWrapper.txs文件应类似于以下内容:

import React from 'react';
import { StyleSheet, View, ActivityIndicator } from 'react-native';
import { RealmProvider } from '@realm/react';
import { App } from './App';
import { Item } from './ItemSchema';
const LoadingIndicator = () => {
return (
<View style={styles.activityContainer}>
<ActivityIndicator size="large" />
</View>
);
};
export const AppWrapper = () => {
return (
<RealmProvider schema={[Item]} fallback={LoadingIndicator}>
<App />
</RealmProvider>
);
};
const styles = StyleSheet.create({
activityContainer: {
flex: 1,
flexDirection: 'row',
justifyContent: 'space-around',
padding: 10,
},
});

接下来,打开 source/App.tsx,并删除有关 dataExplorerLink 的部分以及 OfflineModeLogout 的标题按钮(这将在稍后实现)。

更新后的 App.tsx文件应类似于以下内容:

import React from 'react';
import { SafeAreaProvider } from 'react-native-safe-area-context';
import { StyleSheet, Text, View } from 'react-native';
import { NavigationContainer } from '@react-navigation/native';
import { createStackNavigator } from '@react-navigation/stack';
import { LogoutButton } from './LogoutButton';
import { ItemListView } from './ItemListView';
import { OfflineModeButton } from './OfflineModeButton';
const Stack = createStackNavigator();
const headerRight = () => {
return <OfflineModeButton />;
};
const headerLeft = () => {
return <LogoutButton />;
};
export const App = () => {
return (
<>
{/* All screens nested in RealmProvider have access
to the configured realm's hooks. */}
<SafeAreaProvider>
<NavigationContainer>
<Stack.Navigator>
<Stack.Screen
name="Your To-Do List"
component={ItemListView}
options={{
headerTitleAlign: 'center',
//headerLeft,
//headerRight,
}}
/>
</Stack.Navigator>
</NavigationContainer>
<View style={styles.footer}>
<Text style={styles.footerText}>
Log in with the same account on another device or simulator to see
your list sync in real time.
</Text>
</View>
</SafeAreaProvider>
</>
);
};
const styles = StyleSheet.create({
footerText: {
fontSize: 12,
textAlign: 'center',
marginVertical: 4,
},
hyperlink: {
color: 'blue',
},
footer: {
paddingHorizontal: 24,
paddingVertical: 12,
},
});

最后,打开 source/ItemListView.tsx,进行以下更新:

  • 删除 Flexible 同步订阅代码

  • 将用户替换为模拟用户:- const user={ id: 'mockUserId' };

  • 删除所有 dataExplorerer 引用

  • 删除 Show All Tasks 开关的功能(稍后实施)

更新后的 ItemListView.tsx文件应类似于以下内容:

import React, { useCallback, useState, useEffect } from 'react';
import { BSON } from 'realm';
import { useRealm, useQuery } from '@realm/react';
import { SafeAreaProvider } from 'react-native-safe-area-context';
import { Alert, FlatList, StyleSheet, Switch, Text, View } from 'react-native';
import { Button, Overlay, ListItem } from '@rneui/base';
import { CreateToDoPrompt } from './CreateToDoPrompt';
import { Item } from './ItemSchema';
import { colors } from './Colors';
export function ItemListView() {
const realm = useRealm();
const items = useQuery(Item).sorted('_id');
const user = { id: 'mockUserId' };
const [showNewItemOverlay, setShowNewItemOverlay] = useState(false);
const [showAllItems, setShowAllItems] = useState(true);
// createItem() takes in a summary and then creates an Item object with that summary
const createItem = useCallback(
({ summary }: { summary: string }) => {
// if the realm exists, create an Item
realm.write(() => {
return new Item(realm, {
summary,
owner_id: user?.id,
});
});
},
[realm, user],
);
// deleteItem() deletes an Item with a particular _id
const deleteItem = useCallback(
(id: BSON.ObjectId) => {
// if the realm exists, get the Item with a particular _id and delete it
const item = realm.objectForPrimaryKey(Item, id); // search for a realm object with a primary key that is an objectId
if (item) {
if (item.owner_id !== user?.id) {
Alert.alert("You can't delete someone else's task!");
} else {
realm.write(() => {
realm.delete(item);
});
}
}
},
[realm, user],
);
// toggleItemIsComplete() updates an Item with a particular _id to be 'completed'
const toggleItemIsComplete = useCallback(
(id: BSON.ObjectId) => {
// if the realm exists, get the Item with a particular _id and update it's 'isCompleted' field
const item = realm.objectForPrimaryKey(Item, id); // search for a realm object with a primary key that is an objectId
if (item) {
if (item.owner_id !== user?.id) {
Alert.alert("You can't modify someone else's task!");
} else {
realm.write(() => {
item.isComplete = !item.isComplete;
});
}
}
},
[realm, user],
);
return (
<SafeAreaProvider>
<View style={styles.viewWrapper}>
<View style={styles.toggleRow}>
<Text style={styles.toggleText}>Show All Tasks</Text>
<Switch
trackColor={{ true: '#00ED64' }}
onValueChange={() => {
setShowAllItems(!showAllItems);
}}
value={showAllItems}
/>
</View>
<Overlay
isVisible={showNewItemOverlay}
overlayStyle={styles.overlay}
onBackdropPress={() => setShowNewItemOverlay(false)}>
<CreateToDoPrompt
onSubmit={({ summary }) => {
setShowNewItemOverlay(false);
createItem({ summary });
}}
/>
</Overlay>
<FlatList
keyExtractor={item => item._id.toString()}
data={items}
renderItem={({ item }) => (
<ListItem key={`${item._id}`} bottomDivider topDivider>
<ListItem.Title style={styles.itemTitle}>
{item.summary}
</ListItem.Title>
<ListItem.Subtitle style={styles.itemSubtitle}>
<Text>{item.owner_id === user?.id ? '(mine)' : ''}</Text>
</ListItem.Subtitle>
<ListItem.Content>
{!item.isComplete && (
<Button
title="Mark done"
type="clear"
onPress={() => toggleItemIsComplete(item._id)}
/>
)}
<Button
title="Delete"
type="clear"
onPress={() => deleteItem(item._id)}
/>
</ListItem.Content>
</ListItem>
)}
/>
<Button
title="Add To-Do"
buttonStyle={styles.addToDoButton}
onPress={() => setShowNewItemOverlay(true)}
/>
</View>
</SafeAreaProvider>
);
}
const styles = StyleSheet.create({
viewWrapper: {
flex: 1,
},
sectionContainer: {
marginTop: 32,
paddingHorizontal: 24,
},
addToDoButton: {
backgroundColor: colors.primary,
borderRadius: 4,
margin: 5,
},
completeButton: {
backgroundColor: colors.primary,
borderRadius: 4,
margin: 5,
},
showCompletedButton: {
borderRadius: 4,
margin: 5,
},
showCompletedIcon: {
marginRight: 5,
},
itemTitle: {
flex: 1,
},
itemSubtitle: {
color: '#979797',
flex: 1,
},
toggleRow: {
flexDirection: 'row',
alignItems: 'center',
padding: 12,
},
toggleText: {
flex: 1,
fontSize: 16,
},
overlay: {
backgroundColor: 'white',
},
status: {
width: 40,
height: 40,
justifyContent: 'center',
borderRadius: 5,
borderWidth: 1,
borderColor: '#d3d3d3',
backgroundColor: '#ffffff',
alignSelf: 'flex-end',
},
delete: {
alignSelf: 'flex-end',
width: 65,
marginHorizontal: 12,
},
statusCompleted: {
borderColor: colors.purple,
},
statusIcon: {
textAlign: 'center',
fontSize: 17,
color: colors.purple,
},
});

进行这些更改后,应用应该可以在本地数据库上正常运行。

在开始迁移之前,您需要构建并运行更新的应用程序,以验证其是否按预期运行。

对于iOS,运行以下命令:

npx pod-install
npm run ios

对于 Android,运行以下命令:

npm run android

请注意,任何构建错误都不在本文档的讨论范围之内。 如果您遇到与构建相关的问题,请查阅React Native文档,确保您的环境设立正确。

当应用应用运行时,您可以验证基本功能。 您应该能够:

  • 创建新项目

  • 将事项标记为已完成

  • 删除项目

用户界面的屏幕截图

现在,您已经运行了仅本地Realm应用程序,您可以开始转换此应用程序以使用仅本地版本的 PowerSync客户端。

PowerSync 使用基于 SQLite 的数据库,因此您需要对模式进行一些修改以确保其兼容。

为此,您需要设立PowerSync客户端。 有关详细说明,您可以参阅 @powersync/react-native npm存储库或 PowerSync React Native设置文档。

首先,运行以下命令,为 PowerSync React Native Client、后端 SQLite数据库、异步迭代器 polyfill 添加依赖项(根据说明需要)以及 bson 依赖项(用于生成 ObjectId 以插入文档导入MongoDB中):

npm install @powersync/react-native @journeyapps/react-native-quick-sqlite @azure/core-asynciterator-polyfill bson

要设置polyfill,请打开 index.js,并将 import '@azure/core-asynciterator-polyfill'; 添加到文件顶部。

更新后的 index.js文件应类似于以下内容:

import '@azure/core-asynciterator-polyfill';
import 'react-native-get-random-values';
import {AppRegistry} from 'react-native';
import {AppWrapper} from './source/AppWrapper';
import {name as appName} from './app.json';
AppRegistry.registerComponent(appName, () => AppWrapper);

现在已添加依赖项,您需要重新构建应用程序:

  • 对于iOS,运行pod-install

  • 对于 Android,请将所需的最低 SDK更新为 24,以便与 react-native-quick-sqlite 兼容。 为此,请打开 android/build.gradle 并将 minSdkVersion 从 21 更改为 24。

现在,您将为本地数据库设立数据类型和模式。

请参阅 PowerSync MongoDB类型映射文档,确定如何设立特定模式。以下是可用类型的快速参考:

类型
说明

null

未定义或未设置的值

整型

64 位有符号整数

真实

64 位点

text

UTF-8 文本字符串

blob

二进制数据

在本教程中,您将按如下方式修改 source/ItemSchema.tsx

import {column, Schema, Table} from '@powersync/react-native';
export const ItemSchema = new Table({
isComplete: column.integer,
summary: column.text,
owner_id: column.text,
});
export const AppSchema = new Schema({
Item: ItemSchema,
});
export type Database = (typeof AppSchema)['types'];
export type Item = Database['Item'];

重要

传递给 Schema 的属性名称表示本地表和MongoDB集合的名称。 在本例中,确保其名称为 Item

请注意,此代码直接从 AppSchema 中导出类型,而无需手动定义。

要访问权限PowerSync 并绑定数据,您需要访问权限PowerSync客户端的钩子和提供程序。此功能通过 PowerSyncContext 组件提供。

首先,更新source/AppWrapper.tsx 以使用 PowerSyncContext 并初始化 PowerSync客户端:

import React from 'react';
import {App} from './App';
import {AppSchema} from './ItemSchema';
import {PowerSyncContext, PowerSyncDatabase} from '@powersync/react-native';
const powerSync = new PowerSyncDatabase({
schema: AppSchema,
database: {
dbFilename: 'powersync.db',
},
});
powerSync.init();
export const AppWrapper = () => {
return (
<PowerSyncContext.Provider value={powerSync}>
<App />
</PowerSyncContext.Provider>
);
};

接下来,更新ItemListView.tsx 以使用 PowerSync客户端。 为此,您必须更新此组件顶部使用的钩子:

  • 要访问权限本地数据库以进行写入和更新,请使用 usePowerSync 钩子。

  • 要获取更新时自动重新呈现的待办事项列表项,请使用 useQuery 钩子。

进行以下更改:

  • 删除 import { BSON } from 'realm';

  • 添加 import { ObjectId } from 'bson';

  • 更改 ItemListView 函数的前两行以匹配以下内容:

    export function ItemListView() {
    const db = usePowerSync();
    const {data: items} = useQuery<Item>('SELECT * FROM Item');

接下来,您需要更新createItemdeleteItemtoggleItemIsComplete 方法。

对于其中的每种方法,您都将使用从 usePowerSync 返回的 db对象。 与Realm一样,本地数据库会打开一个ACID 事务来执行任何可变操作,例如插入、更新或删除。 您还将添加 try/catch 块,以将任何错误传播到应用程序的前端。

请注意,此代码会从 bson 导入 ObjectId,以便为每项创建唯一 ID。 请记住,PowerSync 希望主键项目名为 id

创建代码还会直接在此逻辑中实现项目的默认值。 在本例中,isComplete 初始化为 false,id 使用新创建的 ObjectId 的字符串结果进行初始化。

createItem 方法可按如下方式实施:

// createItem() takes in a summary and then creates an Item object with that summary
const createItem = useCallback(
async ({summary}: {summary: string}) => {
try {
// start a write transaction to insert the new Item
db.writeTransaction(async tx => {
await tx.execute(
'INSERT INTO Item (id, summary, owner_id, isComplete) VALUES (?, ?, ?, ?)',
[new ObjectId().toHexString(), summary, user?.id, false],
);
});
} catch (ex: any) {
Alert.alert('Error', ex?.message);
}
},
[db],
);

deleteItemtoggleItemIsComplete 方法类似,因此请按如下方式实现:

// deleteItem() deletes an Item with a particular _id
const deleteItem = useCallback(
async (id: String) => {
// start a write transaction to delete the Item
try {
db.writeTransaction(async tx => {
await tx.execute('DELETE FROM Item WHERE id = ?', [id]);
});
} catch (ex: any) {
Alert.alert('Error', ex?.message);
}
},
[db],
);
// toggleItemIsComplete() updates an Item with a particular _id to be 'completed'
const toggleItemIsComplete = useCallback(
async (id: String) => {
// start a write transaction to update the Item
try {
db.writeTransaction(async tx => {
await tx.execute(
'UPDATE Item SET isComplete = NOT isComplete WHERE id = ?',
[id],
);
});
} catch (ex: any) {
Alert.alert('Error', ex?.message);
}
},
[db],
);

最后,更新呈现的 FlatList。 您将:

  • _id 的实例替换为 id

  • 更新 FlatListkeyExtractor 以直接使用 id 字符串。

  • 以前,数据库会返回 ObjectId。 这需要转换为字符串。

更新后的 FlatList 现在类似于以下内容:

<FlatList
keyExtractor={item => item.id}
data={items}
renderItem={({item}) => (
<ListItem key={`${item.id}`} bottomDivider topDivider>
<ListItem.Title style={styles.itemTitle}>
{item.summary}
</ListItem.Title>
<ListItem.Subtitle style={styles.itemSubtitle}>
<Text>{item.owner_id === user?.id ? '(mine)' : ''}</Text>
</ListItem.Subtitle>
<ListItem.Content>
<Pressable
accessibilityLabel={`Mark task as ${
item.isComplete ? 'not done' : 'done'
}`}
onPress={() => toggleItemIsComplete(item.id)}
style={[
styles.status,
item.isComplete && styles.statusCompleted,
]}>
<Text style={styles.statusIcon}>
{item.isComplete ? '✓' : '○'}
</Text>
</Pressable>
</ListItem.Content>
<ListItem.Content>
<Pressable
accessibilityLabel={'Remove Item'}
onPress={() => deleteItem(item.id)}
style={styles.delete}>
<Text style={[styles.statusIcon, {color: 'blue'}]}>
DELETE
</Text>
</Pressable>
</ListItem.Content>
</ListItem>
)}
/>

更新完代码后,您应该能够使用本地 PowerSync客户端。

要进行验证,请重新构建应用程序。 如果您使用的是iOS,请不要忘记使用 npx pod-install更新Pod。

用户界面的屏幕截图

您现在应该拥有一个可以正常运行的应用程序,可以使用 PowerSync 添加、更新和删除待办事项列表项。

如果遇到问题,可以在示例存储库的02 -Migrate-Local-Client 分支中查看点所做的更改。

您的移动应用程序现已准备好从MongoDB实时同步数据。

注意

您可能已经注意到, Realm数据尚未迁移。 本指南假定Atlas中托管的MongoDB 集群是数据的真实来源,并将其同步到应用程序。 迁移本地数据超出了本教程的范围,但可能会在未来的文档中解决。

您现在应该有一个运行的PowerSync 服务,其中包含来自Atlas的同步数据,并且已使用 PowerSync 诊断工具对其进行了验证。

在此阶段,您将将此数据同步到React Native应用程序中。

首先,您需要创建一种方法来为令牌和端点设立一些环境变量。

首先,将 react-native-dotenv 安装到您的开发依赖项中。 这是一个 babel 插件,它会从项目根目录获取 .env文件,并使您能够将环境变量直接导入到应用程序中。

npm install -D react-native-dotenv

接下来,将以下行添加到 babel.config.js文件中:

module.exports = {
presets: ['module:metro-react-native-babel-preset'],
plugins: ['module:react-native-dotenv'],
};

创建一个名为 types 的新目录,并在其中创建一个名为 env.d.ts 的新文件,其中包含我们要导入的以下变量:

declare module '@env' {
export const AUTH_TOKEN: string;
export const POWERSYNC_ENDPOINT: string;
}

您需要从 PowerSync 中检索环境变量所需的值。

  • 在 PowerSync 控制台的左侧边栏中,单击 TodoList 旁边的“...”以打开上下文菜单。

  • 选择“编辑实例”。

  • 复制并保存URL。

用户界面的屏幕截图

接下来,为 subject/user_id 为mockUserId 的实例生成新的开发令牌。复制并保存生成的令牌。

在应用程序项目的根目录中创建 .env文件,然后粘贴刚刚生成的 PowerSync 端点和令牌:

POWERSYNC_ENDPOINT=<endpoint>
AUTH_TOKEN=<dev-token>

您需要稍微重构应用程序,以便它可以连接到 PowerSync实例。

首先,在 source 中创建一个名为 PowerSync.ts 的新文件,然后粘贴以下内容:

import { AppSchema } from './ItemSchema';
import {
AbstractPowerSyncDatabase,
PowerSyncDatabase,
} from '@powersync/react-native';
import { AUTH_TOKEN, POWERSYNC_ENDPOINT } from '@env';
const powerSync = new PowerSyncDatabase({
schema: AppSchema,
database: {
dbFilename: 'powersync.db',
},
});
powerSync.init();
class Connector {
async fetchCredentials() {
return {
endpoint: POWERSYNC_ENDPOINT,
token: AUTH_TOKEN,
};
}
async uploadData(database: AbstractPowerSyncDatabase) {
console.log('Uploading data');
}
}
export const setupPowerSync = (): PowerSyncDatabase => {
const connector = new Connector();
powerSync.connect(connector);
return powerSync;
};
export const resetPowerSync = async () => {
await powerSync.disconnectAndClear();
setupPowerSync();
};

此文件执行以下操作:

  • 创建新的 Connector 类,用于在 PowerSync客户端中设立开发令牌和 PowerSync 端点。

  • 定义一个模拟的 uploadData 函数,该函数将在下一阶段用于将更改推送到Atlas。

  • 定义设立和重置 PowerSync客户端的方法。 重置客户端现在对开发很有用,因为所做的任何更改都将被放入队列中。 在处理这些更改之前,您不会收到任何新的更新。

接下来,更新AppWrapper.tsx 以使用新的 setupPowerSync 方法:

import { PowerSyncContext } from '@powersync/react-native';
import React from 'react';
import { App } from './App';
import { setupPowerSync } from './PowerSync';
const powerSync = setupPowerSync();
export const AppWrapper = () => {
return (
<PowerSyncContext.Provider value={powerSync}>
<App />
</PowerSyncContext.Provider>
);
};

然后,重构 LogoutButton.tsx 以实现resetPowerSync 方法。 将其重命名为 ResetButton.tsx 并更新其内容,如下所示:

import React, { useCallback } from 'react';
import { Pressable, Alert, View, Text, StyleSheet } from 'react-native';
import { colors } from './Colors';
import { resetPowerSync } from './PowerSync';
export function ResetButton() {
const signOut = useCallback(() => {
resetPowerSync();
}, []);
return (
<Pressable
onPress={() => {
Alert.alert('Reset Database?', '', [
{
text: 'Yes, Reset Database',
style: 'destructive',
onPress: () => signOut(),
},
{ text: 'Cancel', style: 'cancel' },
]);
}}>
<View style={styles.buttonContainer}>
<Text style={styles.buttonText}>Reset</Text>
</View>
</Pressable>
);
}
const styles = StyleSheet.create({
buttonContainer: {
paddingHorizontal: 12,
},
buttonText: {
fontSize: 16,
color: colors.primary,
},
});

然后,修改 App.tsx 以在标题左侧显示 Reset 按钮:

  • import { LogoutButton } from './LogoutButton'; 替换为 import { ResetButton } from './ResetButton';

  • headerLeft 中,将现有行替换为 return <ResetButton />;

  • 取消 //headerLeft 行的注释,以便显示“重置”按钮。

您的更改如下所示:

import React from 'react';
import { SafeAreaProvider } from 'react-native-safe-area-context';
import { StyleSheet, Text, View } from 'react-native';
import { NavigationContainer } from '@react-navigation/native';
import { createStackNavigator } from '@react-navigation/stack';
import { ResetButton } from './ResetButton';
import { ItemListView } from './ItemListView';
import { OfflineModeButton } from './OfflineModeButton';
const Stack = createStackNavigator();
const headerRight = () => {
return <OfflineModeButton />;
};
const headerLeft = () => {
return <ResetButton />;
};
export const App = () => {
return (
<>
{/* All screens nested in RealmProvider have access
to the configured realm's hooks. */}
<SafeAreaProvider>
<NavigationContainer>
<Stack.Navigator>
<Stack.Screen
name="Your To-Do List"
component={ItemListView}
options={{
headerTitleAlign: 'center',
headerLeft,
//headerRight,
}}
/>
</Stack.Navigator>
</NavigationContainer>
<View style={styles.footer}>
<Text style={styles.footerText}>
Log in with the same account on another device or simulator to see
your list sync in real time.
</Text>
</View>
</SafeAreaProvider>
</>
);
};
const styles = StyleSheet.create({
footerText: {
fontSize: 12,
textAlign: 'center',
marginVertical: 4,
},
hyperlink: {
color: 'blue',
},
footer: {
paddingHorizontal: 24,
paddingVertical: 12,
},
});

最后,react-native-dotenv 库要求我们的React Native服务器重置并清除缓存,这在向 Babel 添加功能时很正常。

为此,请使用 ctrl-c 关闭所有当前正在运行的React Native实例,然后输入以下命令以运行已清除缓存的实例:

npm start -- --reset-cache

现在应该已设立就绪,可以将Atlas数据同步到React Native应用程序中了。

现在重置应用程序。 如果您以前修改过应用程序的本地数据库,则需要单击新的 Reset 按钮,使用Atlas中存储的内容重置本地数据库。

您现在应该可以看到 mockUserId 的所有待办事项列表项:

用户界面的屏幕截图

如果遇到问题,删除模拟器/模拟器中的应用程序,然后重新构建以从头开始。

如果仍然遇到问题,可以在示例存储库的03 -Sync-Data-From-Atlas 分支中查看点所做的更改。

现在您的数据已同步到移动应用程序中,下一步就是创建一种将本地更改传播到Atlas 的方法。

在此阶段,您将:

  • Connector 中实现 uploadData 方法

  • 创建一个简单的后端服务器来处理来自移动设备的操作

为简便起见,本指南将在本地运行服务器。 对于生产使用案例,您应考虑使用云服务来处理这些请求(例如 JourneyApps 提供无服务器云函数来帮助解决此问题)。

首先查看在移动应用程序中进行本地更改时发送到 uploadData 方法的操作。

source/PowerSync.ts 进行以下更改:

async uploadData(database: AbstractPowerSyncDatabase) {
const batch = await database.getCrudBatch();
console.log('batch', JSON.stringify(batch, null, 2));
}

接下来,您将在移动应用程序中进行更改,包括:

  • 删除项目

  • 将项目切换为完整或不完整

  • 添加新项目

完成 uploadData 方法的实现,以在提取请求中发送此信息。

首先,为 .env 添加新值:

BACKEND_ENDPOINT=http://localhost:8000

types/env.d.ts

declare module '@env' {
export const AUTH_TOKEN: string;
export const POWERSYNC_ENDPOINT: string;
export const BACKEND_ENDPOINT: string;
}

如果您使用的是 Android 模拟器,则必须确保对端口 8000 上的 localhost 的请求从模拟器转发到本地计算机。 要启用此功能,运行以下命令:

adb reverse tcp:8000 tcp:8000

接下来,将 BACKEND_ENDPOINT 添加到 source/PowerSync.ts 中的导入声明:

import { AUTH_TOKEN, POWERSYNC_ENDPOINT, BACKEND_ENDPOINT } from '@env';

然后更新uploadData 方法:

async uploadData(database: AbstractPowerSyncDatabase) {
const batch = await database.getCrudBatch();
if (batch === null) {
return;
}
const result = await fetch(`${BACKEND_ENDPOINT}/update`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(batch.crud),
});
if (!result.ok) {
throw new Error('Failed to upload data');
}
batch.complete();
}

更新后的方法现在将向后端端点发送增删改查操作大量:

  • 如果应用程序处于离线状态,它就会失败。

  • 如果应用程序收到肯定响应,则会将操作标记为已完成,并且将从移动应用程序中删除批处理操作。

现在,在项目中创建一个名为 backend 的新文件夹:

mkdir backend

然后,创建一个 package.json文件:

{
"main": "index.js",
"scripts": {
"start": "node --env-file=.env index.js"
},
"dependencies": {
"express": "^4.21.2",
"mongodb": "^6.12.0"
}
}

package.json 包括一个 start脚本,该脚本将变量从 .env 添加到服务中。

使用之前的Atlas connection string 创建新的 .env

MONGODB_URI=<connection_string>

现在,安装依赖项:

npm install

请注意,本指南不包括如何向此服务添加Typescript和其他工具,但您可以随意这样做。 此外,该指南将验证保持在最低限度,并且仅实施准备来自移动应用程序的数据以插入MongoDB所需的更改。

首先,创建一个包含以下内容的 index.js

const express = require("express");
const { MongoClient, ObjectId } = require("mongodb");
const app = express();
app.use(express.json());
// MongoDB setup
const client = new MongoClient(
process.env.MONGODB_URI || "mongodb://localhost:27017",
);
// Helper function to coerce isComplete to boolean
function coerceItemData(data) {
if (data && "isComplete" in data) {
data.isComplete = !!Number(data.isComplete);
}
return data;
}
async function start() {
await client.connect();
const db = client.db("PowerSync");
const items = db.collection("Item");
app.post("/update", async (req, res) => {
const operations = req.body;
try {
for (const op of operations) {
console.log(JSON.stringify(op, null, 2));
switch (op.op) {
case "PUT":
await items.insertOne({
...coerceItemData(op.data),
_id: new ObjectId(op.id),
});
break;
case "PATCH":
await items.updateOne(
{ _id: new ObjectId(op.id) },
{ $set: coerceItemData(op.data) },
);
break;
case "DELETE":
await items.deleteOne({
_id: new ObjectId(op.id),
});
break;
}
}
res.json({ success: true });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
app.listen(8000, () => {
console.log("Server running on port 8000");
});
}
start().catch(console.error);

请注意,从上述服务中,isComplete 被强制转换为 boolean 值。 这可确保新的待办事项列表项到达MongoDB时附带 truefalse,而不是 10。 还会从 op.id 创建一个 ObjectId实例。 将此项设置为 _id属性将根据MongoDB要求和最佳实践调整数据。

现在您可以启动服务器了:

npm start

移动应用程序应该已经在尝试向此端点发送操作。 console.log声明应显示正在发送的请求,并且更改应传播到Atlas。

您可以通过在Atlas用户界面或MongoDB Compass中查看MongoDB集合来验证这一点。

用户界面的屏幕截图

您现在应该拥有一个功能齐全的移动应用程序,可以将数据同步到Atlas同步数据。 您还可以尝试关闭 WiFi,以测试应用在离线状态下的运行情况。

如果遇到问题,可以在示例存储库的04 -Write-To-Backend 分支中查看点所做的更改。

最后一个阶段介绍如何实现两个可选的应用测试功能,以及如何清理项目中不需要的代码和依赖项。

在创建此应用程序的进程中,省略了以下功能:“显示所有任务”和离线模式开关。这些功能可用于测试应用功能,不适用于生产应用程序。

注意

与这些功能相关的步骤标记为可选。 如果您不感兴趣,请随意跳过这些可选步骤。

要实现可选的“显示全部”切换,将创建第二个存储桶,该存储桶将根据客户端参数激活。您将通过断开当前同步会话并使用新值设立重新连接应用此功能。 该值是一个名为 view_all 的布尔值,它将被用作不安全的后门,以显示集群中曾经创建的所有待办事项列表项。 此功能有助于展示可以根据某些参数动态创建存储桶。

注意

此处使用的方法不安全,因此需要在存储桶上激活 accept_potentially_dangerous_queries 标志才能执行此操作。 完成此操作的一种安全方法是将其基于用户角色,并在其后端数据库中更新用户授权,但这超出了本指南的范围。

要开始使用,请导航到 PowerSync仪表盘并更新同步规则以包含基于所设立的view_all 参数的存储桶:

bucket_definitions:
user_buckets:
parameters:
- SELECT request.user_id() as user_id
data:
- SELECT _id as id FROM "Item" WHERE bucket.user_id = 'global'
OR owner_id = bucket.user_id
view_all_bucket:
accept_potentially_dangerous_queries: true
parameters:
- SELECT (request.parameters() ->> 'view_all') as view_all
data:
- SELECT _id as id FROM "Item" WHERE bucket.view_all = true

请注意,存储桶定义组合在一起,因此当 view_all_bucket 处于活动状态时,它将添加到 user_buckets 数据中。

接下来,更新项目中的 source/PowerSync.ts 以包含一个局部变量,以确定 view_all 标志状态,并将其应用连接实例的参数。

首先,添加 viewAll 参数并更新setupPowerSync 函数:

let viewAll = false;
export const setupPowerSync = (): PowerSyncDatabase => {
const connector = new Connector();
powerSync.connect(connector, {params: {view_all: viewAll}});
return powerSync;
};

然后,添加以下两个函数:

export const resetPowerSync = async () => {
await powerSync.disconnectAndClear();
setupPowerSync();
};
export const toggleViewAll = () => {
viewAll = !viewAll;
resetPowerSync();
};

最后,更新source/ItemListView.tsx

首先,从 PowerSync 导入 toggleViewAll

import { toggleViewAll } from './PowerSync';

然后修改“显示所有任务”开关的 onValueChange 属性以调用 toggleViewAll 方法。 使用以下代码替换 TextSwitch 组件:

<Text style={styles.toggleText}>Show All Tasks</Text>
<Switch
trackColor={{true: '#00ED64'}}
onValueChange={() => {
setShowAllItems(!showAllItems);
toggleViewAll();
}}
value={showAllItems}
/>

现在重新启动应用程序并验证应用是否按预期运行:

用户界面的屏幕截图

要实现可选的离线模式切换,您需要断开同步会话并重新连接。 这样,您就可以在未连接到同步时进行本地更改,并在重新建立同步会话时验证这些更改是否已发送。

您将为连接状态添加一个变量,然后创建一个方法来切换该状态并调用 PowerSync客户端上的 connectdisconnect 方法。

首先,将以下内容添加到 source/PowerSync.ts

let connection = true;
export const toggleConnection = () => {
if (connection) {
powerSync.disconnect();
} else {
setupPowerSync();
}
connection = !connection;
};

接下来,重构 source/OfflineModeButton.tsx 以删除Realm功能,并通过调用新的 toggleConnection 方法进行替换。 您还需要添加一些 import``s:

import { useState } from 'react';
import { Pressable, Text, StyleSheet } from 'react-native';
import { colors } from './Colors';
import {toggleConnection} from './PowerSync';
export function OfflineModeButton() {
const [pauseSync, togglePauseSync] = useState(false);
return (
<Pressable
onPress={() => {
toggleConnection();
togglePauseSync(!pauseSync);
}}>
<Text style={styles.buttonText}>
{pauseSync ? 'Enable Sync' : 'Disable Sync'}
</Text>
</Pressable>
);
}
const styles = StyleSheet.create({
buttonText: {
padding: 12,
color: colors.primary,
},
});

最后,打开 source/App.tsx 并在应用程序的 Stack.Screen 中取消对 headerRight 组件的注释:

<Stack.Screen
name="Your To-Do List"
component={ItemListView}
options={{
headerTitleAlign: 'center',
headerLeft,
headerRight,
}}
/>

现在,通过打开应用的第二个实例来验证更新,然后进行一些更改:

用户界面的屏幕截图

最后,您可以清理项目。

可以安全地删除以下文件:

  • atlasConfig.json

  • source/WelcomeView.tsx

您还可以从 package.json 中删除以下依赖项:

  • @realm/react

  • realm

本指南应该已为您提供了开始 PowerSync迁移之旅的基础。

总而言之,通过阅读本指南,您应该已完成以下操作:

  • 部署包含示例数据的MongoDB 数据库

  • 已部署 PowerSync 服务,用于同步示例数据

  • 了解如何使用诊断工具查看和查询这些数据

  • 将Device Sync移动应用程序转换为仅限本地

  • 从仅限本地的Realm数据库迁移到 PowerSync

  • 设置从 PowerSync 到mobile database的同步

  • 创建后端以将更改从 PowerSync客户端推送到MongoDB

对于后续步骤,请尝试将一小部分移动应用程序进行转换以使用 PowerSync。 请关注未来介绍更高级用例的文档。

后退

迁移到Amazon Web Services AppSync