简介
Supabase是一个开源的Firebase替代品。这是一个大胆的标题,因为Firebase是作为一个完整的解决方案,具有各种功能,如认证、文件存储、无服务器功能、SDK等等。
即使Firebase有大量的功能,Supabase可能更有用,因为它使用开源技术。Supabase让你可以灵活地托管在你的本地机器上、云服务提供商中,甚至作为一个Docker容器。这意味着它是无限制的,所以不存在厂商锁定的问题。
Supabase在引擎盖下使用PostgreSQL作为数据库,并通过他们建立的几个工具来监听实时变化。
目前,Supabase只支持数据库、认证和存储等功能。他们也有无服务器功能,尽管这些功能仍处于开发阶段。
Supabase脱颖而出的原因如下。
- Supabase为你处理扩展问题(尽管它使用SQL数据库)
- 与Firebase不同,你可以进行复杂的查询或文本搜索
- 在Supabase中,数据迁移很直接,因为它使用PostgreSQL,所以你可以通过一个.sql文件导入数据
然而,使用Supabase也有几个缺点。它们包括
- 功能有限
- 它要求你为一个表启用复制功能,以便接收实时更新
- 当启用实时更新时,Supabase的安全策略并不适用。
- 它的SDK只支持JavaScript(对其他语言的支持仍在测试阶段)。
用Supabase进行存储
Supabase提供了开源的对象存储,可以容纳任何文件类型,并具有很高的可扩展性。它提供了一个方便的API,允许自定义策略和权限。
一些功能,如CDN集成和自动转换和优化(调整大小和压缩你的媒体)将很快推出。随着这些功能的增加,Supabase存储将成为Firebase存储的有力竞争者。
用Supabase进行认证
每个Supabase项目都有内置的认证、授权和用户管理,不需要任何其他工具。
Supabase提供了一个简单的API来整合第三方认证服务提供商,如谷歌、苹果、Twitter、Facebook、Github、Azure、Gitlab和Bitbucket。它还支持像SAML这样的企业登录。
Supabase如何管理实时数据
Supabase使用几个工具与PostgreSQL一起给予实时更新。它们如下。
- Realtime允许你监听 PostgreSQL 中的事件,如插入、更新和删除,并使用 WebSockets 将数据转换为 JSON 格式
- Postgres-meta允许你通过REST API查询PostgreSQL
- PostgREST把PostgreSQL数据库变成一个RESTful API
- GoTrue通过一个生成SWT令牌的SWT API来管理用户
- Kong是一个云原生API网关

通过上面的架构图,你可以看到Supabase是如何用PostgreSQL实现实时数据的。
开始使用Supabase
在本节中,让我们看看如何在Supabase中创建一个数据库并启用实时更新。
首先,让我们登录并从 Supabase 仪表板上创建一个组织。然后,在项目标签下,点击创建项目按钮。这将提示你输入数据库名称、密码和你想托管数据库的地区。
接下来,我们需要从项目仪表板的表标签下创建一个表。这一部分将提示你输入数据库名称和数据库的字段(主键和其他)以及数据类型。
我们将创建一个主键,数据类型为UUID,并启用自动生成。
现在我们需要使这个表能够接收实时更新。从左边的侧边栏移到数据库标签。
接下来,选择复制标签。这一部分将显示你所创建的表。像这样为一个特定的表启用复制功能。
现在我们已经创建了一个数据库和一个启用了复制功能的表,让我们看看如何使用Supabase JavaScript SDK的API方法。
Supabase JavaScript API
Supabase JavaScript API 提供了易于理解的方法。由于我们是在处理 SQL,API 方法看起来与 SQL 查询类似。
const { data, error } = await supabase
.from('pokemon')
.insert([
{ name:'Pikachu', power: 'Fire', description: 'Fluffy' },
])
上面的代码是要在一个名为pokemon 的表中插入一条记录。请注意Supabase连接对象是如何选择表以及像SQL查询一样进行操作的。
Supabase 中的数据查询与 SQL 中的 select 语句类似。
let { data: pokemon, error } = await supabase
.from('pokemon')
.select("*")
Supabase 查询对象有更多的过滤选项。这些看起来类似于SQL查询中的where 和like 子句。
.eq() ,
.gt() ,
.lt() ,
.like() ,
.is() ,
.in()
在React中使用Supabase
让我们看看我们如何通过Create React App将Supabase与React集成。在这个例子中,让我们创建一个小型的Pokémon应用程序,维护关于你最喜欢的Pokémon的数据。
首先,让我们创建一个React应用程序。
npx create-react-app supabase-pokemons
现在,让我们安装构建这个Pokémon应用程序所需的依赖项。我们将使用Semantic UI来构建用户界面。
yarn add @supabase/supabase-js semantic-ui-react semantic-ui-css react-router-dom
现在让我们来构建项目目录。由于这是一个小型应用,我们将使用React的Context API。
首先,让我们在项目根目录下创建一个.env文件,其中包含以下密钥。
REACT_APP_SUPABASE_URL= <SUPABASE_URL>
REACT_APP_SUPABASE_KEY= <SUPABASE_KEY>
这些键可以在Supabase仪表盘的设置部分下找到。
现在,让我们在util/connection.js 下用下面的代码片断创建Supabase连接。
import { createClient } from '@supabase/supabase-js';
const REACT_APP_SUPABASE_URL = process.env.REACT_APP_SUPABASE_URL;
const REACT_APP_SUPABASE_KEY = process.env.REACT_APP_SUPABASE_KEY;
export const supabase = createClient(REACT_APP_SUPABASE_URL, REACT_APP_SUPABASE_KEY);
connection.js file
让我们用内置的第三方服务提供商(如谷歌和Github)向应用程序添加登录功能。
const signIn = async () => {
await supabase.auth.signIn({ email: credentials.email, password: credentials.password });
clear();
}
const signUp = async () => {
await supabase.auth.signUp({ email: credentials.email, password: credentials.password })
clear();
}
正如你所看到的,用户管理很容易维护。你只需要几行代码就可以创建它。
与谷歌和Github的整合
接下来,让我们看看如何与谷歌和Github整合。首先,你需要从特定的认证提供者那里创建密匙,并通过仪表板将它们添加到Supabase中。
const gitHub = async () => {
await supabase.auth.signIn({
provider: 'github'
})
}
你可以使用上述代码来整合Supabase支持的任何其他第三方认证供应商。
这只是一个改变提供商名称的问题,Supabase将为你处理其余的事情。
import { useState, useEffect, useContext } from "react"
import AppContext from "../AppContext";
import { useHistory } from "react-router-dom";
import { Grid, GridColumn, GridRow, Form, FormField, Input, Icon, Button, Header, Segment } from "semantic-ui-react"
const initState = { email: '', password: '', passwordConfirm: '' }
function Login({ supabase }) {
let history = useHistory();
const [isSignIn, setSignIn] = useState(false);
const [credentials, setCredentials] = useState(initState);
const { user, isLoggedIn, login, logout } = useContext(AppContext)
useEffect(() => {
const { data: authListener } = supabase.auth.onAuthStateChange(
async (event, session) => {
const currentUser = session?.user;
login(session.user)
}
);
return () => {
authListener?.unsubscribe();
};
}, [user]);
useEffect(() => {
if (isLoggedIn) {
history.push("/home");
}
}, [isLoggedIn])
const onChange = (type, value) => {
setCredentials({ ...credentials, [type]: value })
}
const clear = () => {
setCredentials(initState)
}
const signIn = async () => {
await supabase.auth.signIn({ email: credentials.email, password: credentials.password });
clear();
}
const signUp = async () => {
await supabase.auth.signUp({ email: credentials.email, password: credentials.password })
clear();
}
const gitHub = async () => {
await supabase.auth.signIn({
provider: 'github'
})
}
const google = async () => {
await supabase.auth.signIn({
provider: 'google'
})
}
return (
<Grid padded>
<GridRow>
<GridColumn width={5}></GridColumn>
<GridColumn width={6}></GridColumn>
<GridColumn width={5}></GridColumn>
</GridRow>
<GridRow>
<GridColumn width={5}></GridColumn>
<GridColumn width={6}>
<Segment>
<Form>
<FormField>
<Header as="h5">Email</Header>
<Input placeholder="Email" value={credentials.email} onChange={(e, { value }) => onChange('email', value)}></Input>
</FormField>
<FormField>
<Header as="h5">Password</Header>
<Input placeholder="Password" value={credentials.password} onChange={(e, { value }) => onChange('password', value)}></Input>
</FormField>
{isSignIn ?
<FormField>
<Header as="h5">Confirm Password</Header>
<Input placeholder="Password" value={credentials.passwordConfirm} onChange={(e, { value }) => onChange('passwordConfirm', value)}></Input>
</FormField>
: null}
<FormField>
<Button onClick={() => isSignIn ? setSignIn(false) : signIn()}>Login</Button>
<Button onClick={() => isSignIn ? signUp() : setSignIn(true)}>SignIn</Button>
</FormField>
</Form>
</Segment>
<Segment>
<Grid>
<GridRow>
<GridColumn width={8}>
<Button icon labelPosition='left' fluid onClick={gitHub}>
<Icon name='github' />
Github
</Button>
</GridColumn>
<GridColumn width={8}>
<Button icon labelPosition='left' fluid onClick={google}>
<Icon name='google' />
Google
</Button>
</GridColumn>
</GridRow>
</Grid>
</Segment>
</GridColumn>
<GridColumn width={5}></GridColumn>
</GridRow>
<GridRow>
<GridColumn width={5}></GridColumn>
<GridColumn width={6}></GridColumn>
<GridColumn width={5}></GridColumn>
</GridRow>
</Grid>
)
}
export default Login
Login.js file
创建一个AppContext.js 文件
接下来,让我们为应用程序创建上下文,该应用程序将保存我们的应用程序数据。
在 src 目录下为应用程序上下文添加一个AppContext.js 文件和一个名为AppReducer.js 的还原器。
import { createContext, useReducer } from "react";
import AppReducer from "./AppReducer"
const initialState = {
user: null,
pokemon: null,
pokemons: [],
isEditing: false,
isLoggedIn: false,
}
const AppContex = createContext(initialState)
export const AppContextProvider = ({ children }) => {
const [state, dispatch] = useReducer(AppReducer, initialState);
const login = (data) => { dispatch({ type: 'LOGIN', payload: data }) }
const logout = (data) => { dispatch({ type: 'LOGOUT', payload: data }) }
const getPokemons = (data) => { dispatch({ type: 'GET_POKEMONS', payload: data }) }
const selectPokemon = (data) => { dispatch({ type: 'SELECT_POKEMON', payload: data }) }
const createPokemon = (data) => { dispatch({ type: 'CREATE_POKEMON', payload: data }) }
const updatePokemon = (data) => { dispatch({ type: 'UPDATE_POKEMON', payload: data }) }
const deletePokemon = (data) => { dispatch({ type: 'DELETE_POKEMON', payload: data }) }
return (
<AppContex.Provider value={{ ...state, login, logout, getPokemons, selectPokemon, createPokemon, updatePokemon, deletePokemon }}>
{children}
</AppContex.Provider >
)
}
export default AppContex;
AppContex.js file
const deleteItem = (pokemons, { id }) => {
return pokemons.filter((pokemon) => pokemon.id !== id)
}
const updateItem = (pokemons, data) => {
let pokemon = pokemons.find((pokemon) => pokemon.id === data.id);
let updatedPokemon = { ...pokemon, ...data };
let pokemonIndex = pokemons.findIndex((pokemon) => pokemon.id === data.id);
return [
...pokemons.slice(0, pokemonIndex),
updatedPokemon,
...pokemons.slice(++pokemonIndex),
];
}
const AppReducer = (state, action) => {
switch (action.type) {
case 'GET_POKEMONS':
return {
...state,
pokemons: action.payload
};
case 'SELECT_POKEMON':
return {
...state,
isEditing: true,
pokemon: action.payload
}
case 'CREATE_POKEMON':
return {
...state,
pokemons: [action.payload, ...state.pokemons]
};
case 'UPDATE_POKEMON':
return {
...state,
isEditing: false,
pokemons: updateItem(state.pokemons, action.payload)
};
case 'DELETE_POKEMON':
return {
...state,
pokemons: deleteItem(state.pokemons, action.payload)
};
case 'LOGIN':
return {
...state,
user: action.payload,
isLoggedIn: true
};
case 'LOGOUT':
return {
...state,
user: null,
isLoggedIn: false
};
default:
return state
}
}
export default AppReducer
AppReducer.js file
将数据添加到应用程序中
现在我们开始使用Supabase的第一个用法。在这里,我们将从一个叫做PokemonForm.jsx 的组件开始向Pokémon表添加数据。
在这个文件下,让我们创建两个函数来创建和更新神奇宝贝。
const createPokemon = async ({ name, power, description }) => {
try {
await supabase
.from('pokemon')
.insert([
{ name, power, description }
]);
} catch (error) {
} finally {
clear();
}
}
上面的函数负责创建一个神奇宝贝。由于我们的表有一个UUID类型的ID字段,它将为每条数据行创建一个唯一的ID。
现在注意到Supabase的每个命令都会返回一个承诺,这样你就可以使用Async/Await 来处理异步行动。更新函数将如下所示。
const updatePokemon = async ({ id, name, power, description }) => {
try {
await supabase
.from('pokemon')
.update([
{ name, power, description }
]).match({ id: id })
} catch (error) {
} finally {
clear();
}
}
你可以从下面的片段中参考整个代码。
import { useEffect, useState, useContext } from "react"
import AppContex from "../AppContext"
import { Form, FormField, Header, Input, Button, Segment } from 'semantic-ui-react'
const initState = { name: '', power: '', description: '' }
function PokemonForm({ supabase }) {
const { isEditing, pokemon } = useContext(AppContex)
const [newPokemon, setNewPokemon] = useState(initState);
useEffect(() => {
if (pokemon) {
setNewPokemon(pokemon)
}
}, [pokemon])
const createPokemon = async ({ name, power, description }) => {
try {
await supabase
.from('pokemon')
.insert([
{ name, power, description }
]);
} catch (error) {
} finally {
clear();
}
}
const updatePokemon = async ({ id, name, power, description }) => {
try {
await supabase
.from('pokemon')
.update([
{ name, power, description }
]).match({ id: id })
} catch (error) {
} finally {
clear();
}
}
const onChange = (type, value) => {
setNewPokemon({ ...pokemon, [type]: value })
}
const clear = () => {
setNewPokemon(initState)
}
const cancel = () => {
clear()
}
return (
<Segment>
<Form>
<FormField>
<Header as="h5">Name</Header>
<Input value={newPokemon.name} onChange={(e, { value }) => onChange('name', value)} />
</FormField>
<FormField>
<Header as="h5">Power</Header>
<Input value={newPokemon.power} onChange={(e, { value }) => onChange('power', value)} />
</FormField>
<FormField>
<Header as="h5">Description</Header>
<Input value={newPokemon.description} onChange={(e, { value }) => onChange('description', value)} />
</FormField>
<Button onClick={() => isEditing ? updatePokemon(newPokemon) : createPokemon(newPokemon)}>{isEditing ? 'Update' : 'Save'}</Button>
<Button onClick={() => cancel()}>Cancel</Button>
</Form>
</Segment>
)
}
export default PokemonForm
以同样的方式,你可以通过运行下面的代码删除一个特定的神奇宝贝。
const deletePokemon = async (id) => {
await supabase
.from('pokemon')
.delete().match({ id: id })
}
注意,我们传入了ID(这是Supabase自动生成的UUID),它将根据给定的ID搜索神奇宝贝并执行删除。
创建一个事件监听器
接下来,让我们创建一个事件订阅者,他将听取整个应用程序的实时事件。由于我们要订阅事件,监听它们的理想场所是React中的useEffect 生命周期钩。
让我们在Home.jsx 文件中创建事件监听器。
useEffect(() => {
supabase
.from('pokemon')
.select().then(({ data }) => { getPokemons(data) })
const subscription = supabase
.from('pokemon')
.on('*', payload => {
alterPokemons(payload)
})
.subscribe()
return () => supabase.removeSubscription(subscription)
}, []);
注意我们是如何为应用程序的卸载阶段创建事件监听器和清理函数的,并返回useEffect 。
Supabase对象提供了一个名为.on() 的API函数,它接受两个参数。第一个参数是事件类型,第二个参数是回调函数。
Supabase监听的事件有几个。它们是
INSERT:监听数据插入事件UPDATE:监听数据更新事件DELETE:监听数据删除事件*:监听通过应用程序发生的所有事件
现在,为了监听应用程序中发生的所有事件,让我们创建一个函数,该函数将根据事件类型启动一个还原器函数。
const alterPokemons = (payload) => {
switch (payload.eventType) {
case "INSERT":
createPokemon(payload.new);
break;
case "DELETE":
deletePokemon(payload.old);
break;
case "UPDATE":
return updatePokemon(payload.new)
default:
createPokemon(payload.new);
}
}
这个函数将在.on() 函数内触发。请注意,有效载荷会返回三个重要的值。
它们是
eventType: 事件类型INSERT,UPDATE, 和DELETEnew: 新数据/更新的数据old:旧数据
通过上面的代码片段,你可以看到为什么Supabase正在成为Firebase的一个有竞争力的替代品。与其他服务相比,Supabase的API只用几行代码就能提供所有很酷的功能。
你可以通过下面的GitHub repo来查看这个项目的代码。
结论
总之,Supabase是 谷歌Firebase的最佳开源替代品。它用PostgreSQL的力量提供了一些很酷的功能,而且它不像其他实时数据库解决方案那样对数据类型有任何限制。
你可以通过参考Supabase的文档找到更多关于它的信息。
感谢你花时间阅读这篇文章。我希望在下面的评论区看到你对这个话题的问题和评论。干杯!
Supabase介绍一文首次出现在LogRocket博客上。