快速入门|使用MemFire Cloud构建React Native应用程序

艾木酱
• 阅读 798

MemFire Cloud是一款提供云数据库,用户可以创建云数据库,并对数据库进行管理,还可以对数据库进行备份操作。它还提供后端即服务,用户可以在1分钟内新建一个应用,使用自动生成的API和SDK,访问云数据库、对象存储、用户认证与授权等功能,可专注于编写前端应用程序代码,加速WEB或APP应用开发。

视频教学链接

此示例提供了使用 MemFire Cloud 和 React Native构建简单用户管理应用程序(从头开始)的步骤。这包括:

  • MemFire Cloud云数据库:用于存储用户数据的 MemFireDB数据库。

  • MemFire Cloud用户验证:用户可以使用邮箱密码登录。

  • MemFire Cloud存储:用户可以上传照片。

  • 行级安全策略:数据受到保护,因此个人只能访问自己的数据。

  • 即时API:创建数据库表时会自动生成 API。

在本指南结束时,您将拥有一个允许用户登录和更新一些基本个人资料详细信息的应用程序:

快速入门|使用MemFire Cloud构建React Native应用程序

创建应用

目的:我们的应用就是通过在这里创建的应用来获得数据库、云存储等一系列资源,并将获得该应用专属的API访问链接和访问密钥,用户可以轻松的与以上资源进行交互。

登录https://cloud.memfiredb.com/auth/login创建应用

快速入门|使用MemFire Cloud构建React Native应用程序

创建数据表

点击应用,视图化创建数据表

  1. 创建profiles表;

在数据表页面,点击“新建数据表”,页面配置如下:

快速入门|使用MemFire Cloud构建React Native应用程序

其中profiles表字段id和auth.users表中的id字段(uuid类型)外键关联。

  1. 开启Profiles的RLS数据安全访问规则;

选中创建的Profiles表,点击表权限栏,如下图所示,点击"启用RLS"按钮

快速入门|使用MemFire Cloud构建React Native应用程序

  1. 允许每个用户可以查看公共的个人信息资料;

点击"新规则"按钮,在弹出弹框中,选择"为所有用户启用访问权限",输入策略名称,选择"SELECT(查询)"操作,点击“创建策略”按钮,如下图。

快速入门|使用MemFire Cloud构建React Native应用程序

  1. 仅允许用户增删改查本人的个人资料信息;

点击"新规则"按钮,在弹出弹框中,选择"根据用户ID为用户启用访问权限",输入策略名称,选择"ALL(所有)"操作,点击“创建策略”按钮,如下图。

快速入门|使用MemFire Cloud构建React Native应用程序

创建avatars存储桶

创建云存储的存储桶,用来存储用户的头像图片,涉及操作包括:

  1. 创建一个存储桶avatars

在该应用的云存储导航栏,点击“新建Bucket”按钮,创建存储桶avatars。

快速入门|使用MemFire Cloud构建React Native应用程序

  1. 允许每个用户可以查看存储桶avatars

选中存储桶avatars,切换到权限设置栏,点击“新规则”按钮,弹出策略编辑弹框,选择“自定义”,如下图所示:

快速入门|使用MemFire Cloud构建React Native应用程序

选择SELECT操作,输入策略名称,点击“生成策略”按钮,如下图所示。

快速入门|使用MemFire Cloud构建React Native应用程序

  1. 允许用户上传存储桶avatars;

选中存储桶avatars,切换到权限设置栏,点击“新规则”按钮,弹出策略编辑弹框,选择“自定义”,如下图所示:

快速入门|使用MemFire Cloud构建React Native应用程序

选择INSERT操作,输入策略名称,点击“生成策略”按钮,如下图所示。

快速入门|使用MemFire Cloud构建React Native应用程序

查看结果

快速入门|使用MemFire Cloud构建React Native应用程序

所有数据表及RLS的sql(策略名称用英文代替)

-- Create a table for public "profiles"
create table profiles (
  id uuid references auth.users not null,
  updated_at timestamp with time zone,
  username text unique,
  avatar_url text,
  website text,

  primary key (id),
  unique(username),
);

alter table profiles enable row level security;

create policy "Public profiles are viewable by everyone."
  on profiles for select
  using ( true );

create policy "Users can insert their own profile."
  on profiles for insert
  with check ( auth.uid() = id );

create policy "Users can update own profile."
  on profiles for update
  using ( auth.uid() = id );
-- Set up Storage!
insert into storage.buckets (id, name)
values ('avatars', 'avatars');

create policy "Avatar images are publicly accessible."
  on storage.objects for select
  using ( bucket_id = 'avatars' );

create policy "Anyone can upload an avatar."
  on storage.objects for insert
  with check ( bucket_id = 'avatars' );

获取 API密钥

现在您已经创建了一些数据库表,您可以使用自动生成的 API 插入数据。我们只需要从API设置中获取您在上面复制的URL和anon的密钥。

在应用->概括页面,获取服务地址以及token信息。

快速入门|使用MemFire Cloud构建React Native应用程序

Anon(公开)密钥是客户端API密钥。它允许“匿名访问”您的数据库,直到用户登录。登录后,密钥将切换到用户自己的登录令牌。这将为数据启用行级安全性。

注意:service_role(秘密)密钥可以绕过任何安全策略完全访问您的数据。这些密钥必须保密,并且要在服务器环境中使用,绝不能在客户端或浏览器上使用。 在后续示例代码中,需要提供supabaseUrl和supabaseKey。

认证设置

当用户点击邮件内魔法链接进行登录时,是需要跳转到我们应用的登录界面的。这里需要在认证设置中进行相关URL重定向的配置。

因为我们最终的应用会在本地的3000端口启动(亦或者其他端口),所以这里我们暂时将url设置为http://localhost:3000/

除此之外,在此界面也可以自定义使用我们自己的smtp服务器。

快速入门|使用MemFire Cloud构建React Native应用程序

构建应用程序

让我们从头开始构建 React Native应用程序。

初始化 React Native

我们可以使用create-expo-app来初始化一个名为 memfiredbReactNative:

npx create-expo-app memfiredbReactNative
cd memfiredbReactNative

然后让我们安装额外的依赖项:supabase-js

yarn add @supabase/supabase-js
yarn add react-native-elements
yarn add react-native-safe-area-context
yarn add @react-native-async-storage/async-storage
yarn add react-native-url-polyfill

我们需要创建一个可以访问我们应用程序数据的客户端,我们使用了Supabase 客户端,使用他生态里提供的功能(登录、注册、增删改查等)去进行交互。创建一个可以访问我们应用程序数据的客户端需要接口的地址(URL)和一个数据权限的令牌(ANON_KEY),我们需要去应用的概览里面去获取这两个参数然后配置到supabase.js里面去。

lib/supabase.js文件

import AsyncStorage from '@react-native-async-storage/async-storage';
import { createClient } from '@supabase/supabase-js'

const URL = ""
const ANON_KEY = ""

export const supabase = createClient(URL, ANON_KEY, {
  localStorage: AsyncStorage,
  autoRefreshToken: true,
  persistSession: true,
  detectSessionInUrl: false,
});

让我们设置一个 React Native 组件来管理登录和注册。用户将能够使用他们的电子邮件和密码登录。

components/Auth.js

import React, { useState } from 'react'
import { Alert, StyleSheet, View } from 'react-native'
import { supabase } from '../lib/supabase'
import { Button, Input } from 'react-native-elements'

export default function Auth() {
  const [email, setEmail] = useState('')
  const [password, setPassword] = useState('')
  const [loading, setLoading] = useState(false)

  async function signInWithEmail() {
    setLoading(true)
    const { user, error } = await supabase.auth.signIn({
      email: email,
      password: password,
    })

    if (error) Alert.alert(error.message)
    setLoading(false)
  }

  async function signUpWithEmail() {
    setLoading(true)
    const { user, error } = await supabase.auth.signUp({
      email: email,
      password: password,
    })

    if (error) Alert.alert(error.message)
    setLoading(false)
  }

  return (
    <View style={styles.mt100}>
      <View style={[styles.verticallySpaced, styles.mt20]}>
        <Input
          label="邮箱"
          leftIcon={{ type: 'font-awesome', name: 'envelope' }}
          onChangeText={(text) => setEmail(text)}
          value={email}
          placeholder="email@address.com"
          autoCapitalize={'none'}
        />
      </View>
      <View style={styles.verticallySpaced}>
        <Input
          label="密码"
          leftIcon={{ type: 'font-awesome', name: 'lock' }}
          onChangeText={(text) => setPassword(text)}
          value={password}
          secureTextEntry={true}
          placeholder="Password"
          autoCapitalize={'none'}
        />
      </View>
      <View style={[styles.verticallySpaced, styles.mt20]}>
        <Button title="登录" disabled={loading} onPress={() => signInWithEmail()} />
      </View>
      <View style={styles.verticallySpaced}>
        <Button title="注册" disabled={loading} onPress={() => signUpWithEmail()} />
      </View>
    </View>
  )
}

const styles = StyleSheet.create({
  container: {
    marginTop: 40,
    padding: 12,
  },
  verticallySpaced: {
    paddingTop: 4,
    paddingBottom: 4,
    alignSelf: 'stretch',
  },
  mt20: {
    marginTop: 20,
  },
  mt100:{
    marginTop:100
  }
})

用户信息页面

用户登录后,我们可以允许他们编辑他们的个人资料详细信息并管理他们的帐户。

让我们为它创建一个名为Account.js.

components/Account.js

import { useState, useEffect } from "react";
import { supabase } from "../lib/supabase";
import { StyleSheet, View, Alert } from "react-native";
import { Button, Input } from "react-native-elements";
import { ApiError, Session } from "@supabase/supabase-js";

export default function Account({ session }) {
  const [loading, setLoading] = useState(false);
  const [username, setUsername] = useState("");
  const [website, setWebsite] = useState("");
  const [avatar_url, setAvatarUrl] = useState("");

  useEffect(() => {
    if (session) getProfile();
  }, [session]);

  async function getProfile() {
    try {
      setLoading(true);
      const user = supabase.auth.user();
      if (!user) throw new Error("No user on the session!");

      let { data, error, status } = await supabase
        .from("profiles")
        .select(`username, website, avatar_url`)
        .eq("id", user.id)
        .single();
      if (error && status !== 406) {
        throw error;
      }

      if (data) {
        setUsername(data.username);
        setWebsite(data.website);
        setAvatarUrl(data.avatar_url);
      }
    } catch (error) {
      Alert.alert((error).message);
    } finally {
      setLoading(false);
    }
  }

  async function updateProfile({
    username,
    website,
    avatar_url,
  }) {
    try {
      setLoading(true);
      const user = supabase.auth.user();
      if (!user) throw new Error(" 会话中没有用户!");

      const updates = {
        id: user.id,
        username,
        website,
        avatar_url,
        updated_at: new Date(),
      };

      let { error } = await supabase
        .from("profiles")
        .upsert(updates, { returning: "minimal" });

      if (error) {
        throw error;
      }
    } catch (error) {
      Alert.alert((error).message);
    } finally {
      setLoading(false);
    }
  }

  return (
    <View style={styles.mt100}>
        <View>

        </View>
      <View style={[styles.verticallySpaced, styles.mt20]}>
        <Input label="邮箱" value={session?.user?.email} disabled />
      </View>
      <View style={styles.verticallySpaced}>
        <Input
          label="用户名"
          value={username || ""}
          onChangeText={(text) => setUsername(text)}
        />
      </View>
      <View style={styles.verticallySpaced}>
        <Input
          label="网址"
          value={website || ""}
          onChangeText={(text) => setWebsite(text)}
        />
      </View>

      <View style={[styles.verticallySpaced, styles.mt20]}>
        <Button
          title={loading ? "正在修改" : "确定"}
          onPress={() => updateProfile({ username, website, avatar_url })}
          disabled={loading}
        />
      </View>

      <View style={styles.verticallySpaced}>
        <Button title="登出" onPress={() => supabase.auth.signOut()} />
      </View>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    marginTop: 40,
    padding: 12,
    backgroundColor:'black'
  },
  verticallySpaced: {
    paddingTop: 4,
    paddingBottom: 4,
    alignSelf: "stretch",
  },
  mt20: {
    marginTop: 20,
  },
  mt100:{
    marginTop:100
  }
});

修改项目入口文件

现在我们已经准备好所有组件,让我们更新App.js:

App.js

import { StatusBar } from 'expo-status-bar';
import { StyleSheet, Text, View } from 'react-native';
import 'react-native-url-polyfill/auto'
import React, { useRef, useState, useEffect } from 'react';
import { supabase } from './lib/supabase'
import Auth from './components/Auth'
import Account from './components/Account'

export default function App() {
  const [session, setSession] = useState()

  useEffect(() => {
    setSession(supabase.auth.session())

    supabase.auth.onAuthStateChange((_event, session) => {
      setSession(session)
    })
  }, [])

  return (
    <View>
      {session && session.user ? <Account key={session.user.id} session={session} /> : <Auth />}
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: 'black',
    alignItems: 'center',
    justifyContent: 'center',
  },
});

完成后,在终端执行打开浏览器

expo start --web

快速入门|使用MemFire Cloud构建React Native应用程序

实现:上传头像及更新用户信息

每个 MemFire Cloud项目都配置了存储,用于管理照片和视频等大文件。

创建上传小组件

让我们为用户创建一个头像,以便他们可以上传个人资料照片。

创建上传头像的小组件

components/Avatar.js

import { useEffect, useState } from 'react'
import { supabase } from '../lib/supabase'
import { View } from 'react-native'
import { Button } from 'react-native-elements'

export default function Avatar({ url, size, onUpload }) {
  const [avatarUrl, setAvatarUrl] = useState(null)
  const [uploading, setUploading] = useState(false)

  useEffect(() => {
    if (url) downloadImage(url)
  }, [url])

  async function downloadImage(path) {
    try {
      const { data, error } = await supabase.storage.from('avatars').download(path)
      if (error) {
        throw error
      }
      var blob = new Blob([data], {
        type: "text/vtt; charset=utf-8"
    });

    const fileReaderInstance = new FileReader();
    fileReaderInstance.readAsDataURL(blob);
    fileReaderInstance.onload = () => {
        let base64 = fileReaderInstance.result;
        setAvatarUrl(base64)
    }
    } catch (error) {
      console.log('Error downloading image: ', error.message)
    }
  }
  async function uploadAvatar(event) {
    try {
      setUploading(true)

      if (!event.target.files || event.target.files.length === 0) {
        throw new Error('You must select an image to upload.')
      }

      const file = event.target.files[0]
      const fileExt = file.name.split('.').pop()
      const fileName = `${Math.random()}.${fileExt}`
      const filePath = `${fileName}`

      let { error: uploadError } = await supabase.storage.from('avatars').upload(filePath, file)
      if (uploadError) {
        throw uploadError
      }

      onUpload(filePath)
    } catch (error) {
      alert(error.message)
    } finally {
      setUploading(false)
    }
  }

  return (
    <View>
      {avatarUrl ? (
        <img src={avatarUrl} alt="Avatar" className="avatar image" />
      ) : (
        <View className="avatar no-image" />
      )}
      <View>
      <label className="button primary block" htmlFor="single">
        <Button  title={uploading ? 'Uploading ...' : 'Upload'}></Button>

        </label>
      <input
          style={{
            visibility: 'hidden',
            position: 'absolute',
          }}
          type="file"
          id="single"
          accept="image/*"
          onChange={uploadAvatar}
          disabled={uploading}
        />
      </View>
    </View>
  )
}

然后我们可以在components/Account.js模板的顶部添加小部件:

components/Account.js

import { useState, useEffect } from "react";
import { supabase } from "../lib/supabase";
import { StyleSheet, View, Alert } from "react-native";
import { Button, Input } from "react-native-elements";
import { ApiError, Session } from "@supabase/supabase-js";
import Avatar from './Avatar'

export default function Account({ session }) {
  const [loading, setLoading] = useState(false);
  const [username, setUsername] = useState("");
  const [website, setWebsite] = useState("");
  const [avatar_url, setAvatarUrl] = useState("");

  useEffect(() => {
    if (session) getProfile();
  }, [session]);

  async function getProfile() {
    try {
      setLoading(true);
      const user = supabase.auth.user();
      if (!user) throw new Error("No user on the session!");

      let { data, error, status } = await supabase
        .from("profiles")
        .select(`username, website, avatar_url`)
        .eq("id", user.id)
        .single();
      if (error && status !== 406) {
        throw error;
      }

      if (data) {
        setUsername(data.username);
        setWebsite(data.website);
        setAvatarUrl(data.avatar_url);
      }
    } catch (error) {
      Alert.alert((error).message);
    } finally {
      setLoading(false);
    }
  }

  async function updateProfile({
    username,
    website,
    avatar_url,
  }) {
    try {
      setLoading(true);
      const user = supabase.auth.user();
      if (!user) throw new Error(" 会话中没有用户!");

      const updates = {
        id: user.id,
        username,
        website,
        avatar_url,
        updated_at: new Date(),
      };

      let { error } = await supabase
        .from("profiles")
        .upsert(updates, { returning: "minimal" });

      if (error) {
        throw error;
      }
    } catch (error) {
      Alert.alert((error).message);
    } finally {
      setLoading(false);
    }
  }

  return (
    <View style={styles.mt100}>
        <View>
        <Avatar url={avatar_url}
      size={150}
      onUpload={(url) => {
        setAvatarUrl(url)
        updateProfile({ username, website, avatar_url: url })
      }}/>
        </View>
      <View style={[styles.verticallySpaced, styles.mt20]}>
        <Input label="邮箱" value={session?.user?.email} disabled />
      </View>
      <View style={styles.verticallySpaced}>
        <Input
          label="用户名"
          value={username || ""}
          onChangeText={(text) => setUsername(text)}
        />
      </View>
      <View style={styles.verticallySpaced}>
        <Input
          label="网址"
          value={website || ""}
          onChangeText={(text) => setWebsite(text)}
        />
      </View>

      <View style={[styles.verticallySpaced, styles.mt20]}>
        <Button
          title={loading ? "正在修改" : "确定"}
          onPress={() => updateProfile({ username, website, avatar_url })}
          disabled={loading}
        />
      </View>

      <View style={styles.verticallySpaced}>
        <Button title="登出" onPress={() => supabase.auth.signOut()} />
      </View>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    marginTop: 40,
    padding: 12,
    backgroundColor:'black'
  },
  verticallySpaced: {
    paddingTop: 4,
    paddingBottom: 4,
    alignSelf: "stretch",
  },
  mt20: {
    marginTop: 20,
  },
  mt100:{
    marginTop:100
  }
});

恭喜!在这个阶段,您拥有一个功能齐全的应用程序!

点赞
收藏
评论区
推荐文章
艾木酱 艾木酱
2年前
超轻量级的VSCode插件,数据库想用就用~
MemFireCloud是一个便捷、灵活、高效的云服务平台,依托于分布式数据库MemFireDB的云原生和线性扩展能力,为互联网用户提供一站式数据库自助服务。介绍MemFireCloud推出VSCode的可视化数据库管理插件,提供操作数据库的图形界面,帮助开发人员轻松简单的写代码,边操作数据库。其主要功能包括:连接管理:统一管理所有的数据库连接;
艾木酱 艾木酱
2年前
快速入门|使用MemFire Cloud构建Vue3应用程序
MemFireCloud是一款提供云数据库,用户可以创建云数据库,并对数据库进行管理,还可以对数据库进行备份操作。它还提供后端即服务,用户可以在1分钟内新建一个应用,使用自动生成的API和SDK,访问云数据库、对象存储、用户认证与授权等功能,可专注于编写前端应用程序代码,加速WEB或APP应用开发。此示例提供了使用MemFireCloud和Vue3
艾木酱 艾木酱
2年前
快速入门|使用MemFire Cloud构建Next.js应用程序
MemFireCloud是一款提供云数据库,用户可以创建云数据库,并对数据库进行管理,还可以对数据库进行备份操作。它还提供后端即服务,用户可以在1分钟内新建一个应用,使用自动生成的API和SDK,访问云数据库、对象存储、用户认证与授权等功能,可专注于编写前端应用程序代码,加速WEB或APP应用开发。此示例提供了使用MemFireCloud和Next
艾木酱 艾木酱
2年前
系统公告 | MemFire Cloud推出微信小程序SDK
“超能力”数据库~拿来即用,应用开发人员再也不用为撰写API而发愁。MemFireCloud为开发者提供了简单易用的云数据库(表编辑器、自动生成API、SQL编辑器、备份恢复、托管运维),很大地降低开发者的使用门槛。简介MemFireCloud推出基于SupabaseJavaScript库进行改造的supabasewechatstableSDK,
Wesley13 Wesley13
3年前
vivo 云服务海量数据存储架构演进与实践
一、写在开头vivo云服务提供给用户备份手机上的联系人、短信、便签、书签等数据的能力,底层存储采用MySQL数据库进行数据存储。随着vivo云服务业务发展,云服务用户量增长迅速,存储在云端的数据量越来越大,海量数据给后端存储带来了巨大的挑战。云服务业务这几年最大的痛点,就是如何解决用户海量数据的存储问题。二、面临挑战
云数据库与Web网站:构建高效、可扩展的网络应用
云数据库与Web网站之间存在着密切的关系。Web网站需要数据库来存储和管理用户数据、交易信息、内容资料等关键信息。而云数据库提供的弹性和可扩展性,使得Web网站能够根据业务需求快速地增加或减少存储资源,从而更加高效地应对用户访问和数据增长。
艾木酱 艾木酱
2年前
快速入门|使用MemFire Cloud构建Flutter应用程序
MemFireCloud是一款提供云数据库,用户可以创建云数据库,并对数据库进行管理,还可以对数据库进行备份操作。它还提供后端即服务,用户可以在1分钟内新建一个应用,使用自动生成的API和SDK,访问云数据库、对象存储、用户认证与授权等功能,可专注于编写前端应用程序代码,加速WEB或APP应用开发。此示例提供了使用MemFireCloud和Flut
艾木酱 艾木酱
2年前
应用实战|微信小程序开发示例之Super课表
此示例提供了使用MemFireCloud构建一个课表的小程序的步骤。小程序用到的MemFireCloud的功能包括:云数据库:存储小程序数据表的信息。用户验证:小程序使用MemFireCloud提供的用户认证的API接口,快速完成用户注册登录操作。云存储:存储小程序的注册用户上传的头像。行级安全策略:采用RLS策略来限制用户访问行为,用户可
艾木酱 艾木酱
2年前
应用实战|使用DBGate管理MemFireDB数据库
MemFireCloud是一款提供云数据库,用户可以创建云数据库,并对数据库进行管理,还可以对数据库进行备份操作。它还提供后端即服务,用户可以在1分钟内新建一个应用,使用自动生成的API和SDK,访问云数据库、对象存储、用户认证与授权等功能,可专注于编写前端应用程序代码,加速WEB或APP应用开发。简介DbGate是一款开源、开源、跨平台的数据库管理功能