快速上手 Supabase 数据库:构建现代 Web 应用的利器

9,026 阅读5分钟

image.png

在当今快速发展的 Web 开发领域,选择合适的后端服务对于项目的成功至关重要。Supabase 是一个开源的 Firebase 替代方案,它提供了一个完整的后端解决方案,包括实时数据库、认证、存储、函数等功能。本文将带你快速上手 Supabase 数据库,帮助你在实际开发中更高效地构建应用。

什么是 Supabase?

Supabase 是一个基于 PostgreSQL 的开源后端即服务(BaaS),它提供了易于使用的 API 来处理常见的后端任务,如用户认证、数据库操作、文件存储和服务器端函数等。Supabase 的目标是简化 Web 和移动应用的开发过程,让开发者能够专注于核心业务逻辑。

image.png

创建 Supabase 项目

首先,你需要在 Supabase 官网 注册一个账号并创建一个新的项目。创建项目时,你需要填写项目名称、选择一个区域以及设置数据库密码。

  1. 创建一个组织 image.png

  2. 在刚刚创建的组织下面创建一个新的项目 image.png

配置数据库

Supabase 使用 PostgreSQL 作为其底层数据库,因此你可以使用 SQL 来管理你的数据。Supabase 提供了一个在线编辑器来帮助你创建和管理数据库表。

创建表

假设我们要为一个简单的博客应用创建一个 users 表,包含以下字段:

  • id: 主键,自增整数。
  • username: 用户名,字符串类型。
  • password: 密码,文本类型。
  • email: 邮箱,文本类型。
  • tel: 电话,文本类型。
  • avatar: 头像地址,文本类型。
  • email: 邮箱,文本类型。
  • gender: 性别,文本类型。
  • created_at: 创建时间,默认为当前时间戳。

我们可以通过 Supabase 的在线表格编辑器很方便地创建这个表:

image.png

image.png

image.png

使用 Supabase 客户端进行 CRUD 操作

image.png

Supabase 提供了很多的客户端库和示例代码,这里我们使用 JavaScript 客户端库,方便我们在前端代码中与数据库进行交互。首先,我们需要安装 Supabase 客户端库:

npm install @supabase/supabase-js
# or CDN
<script src="https://unpkg.com/@supabase/supabase-js@2"></script>

然后,在我们的应用中初始化 Supabase 客户端:

import { createClient } from '@supabase/supabase-js';

const supabaseUrl = 'https://your-project-url.supabase.co';
const supabaseKey = 'your-anon-key';
const supabase = createClient(supabaseUrl, supabaseKey);

您可以在控制面板的 Project Settings/API 里面找到 supabaseUrlsupabaseKey

image.png

创建(Create)

要向 users 表中插入新数据,可以使用 insert 方法:

async function createUser(data) {
    const { data, error } = await supabase
        .from('users')
        .insert(data);
    if (error) console.error(error);
    else console.log(data);
}

createUser({
    username: '小笑残虹',
    password: '123456',
    email: 'xxxx@xcmexico.com',
    tel: '063-01138859',
    avatar: 'http://www.xxx.com/a.png',
    gender: '男',
});

读取(Read)

要从 users 表中获取所有数据,可以使用 select 方法:

async function getUsers() {
    const { data, error } = await supabase
        .from('users')
        .select('*');
    if (error) console.error(error);
    else console.log(data);
}

getUsers();

更新(Update)

要更新 users 表中的某条数据,可以使用 update 方法:

async function updateUser(id, data) {
    const { data, error } = await supabase
        .from('users')
        .update(data)
        .eq('id', id);
    if (error) console.error(error);
    else console.log(data);
}

updateUser(1, { password: '11111' });

删除(Delete)

要从 users 表中删除某条数据,可以使用 delete 方法:

async function deleteUser(id) {
    const { data, error } = await supabase
        .from('users')
        .delete()
        .eq('id', id);
    if (error) console.error(error);
    else console.log(data);
}

deleteUser(1);

🚀 实战演练

学习了以上基础知识后,相信大家对 Supabase 有了一个基本的认识,接下来我们再通过一个简单的小项目,带大家深入地探索一下 Supabase,涵盖了开发中常见的增删改查等操作。

项目预览

  • 支持分页查询、模糊查询
  • 列表排序功能
  • 创建数据
  • 编辑数据
  • 删除数据
  • 通过 faker.js 生成测试数据

image.png

创建用户 image.png

模糊查询 image.png

环境搭建

接下来,我们使用 vuejs 结合 ElementPlus 组件库来快速搭建页面的基本结构,新建一个 html 文件:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Supabase Demo</title>
    <link rel="stylesheet" href="https://unpkg.com/element-plus/dist/index.css">
    <!-- vuejs -->
    <script src="https://unpkg.com/vue@3"></script>
    <!-- supabase -->
    <script src="https://unpkg.com/@supabase/supabase-js@2"></script>
    <!-- element-plus -->
    <script src="https://unpkg.com/element-plus"></script>
    <!-- element-plus 中文包 -->
    <script src="//unpkg.com/element-plus/dist/locale/zh-cn"></script>
</head>

<body>
    <div id="app">
        <el-button type="primary">{{ msg }}</el-button>
    </div>
</body>

<script>
    const { createApp, ref } = Vue;

    const app = createApp({
        setup() {
            const msg = ref('Hello Supabase');

            return {
                msg,
            }
        }
    });

    app.use(ElementPlus, {
        locale: ElementPlusLocaleZhCn,
    });
    app.mount('#app');
</script>

</html>

页面预览

image.png

至此我们的 vuejs 和 ElementPlus 就配置好了。

构建页面

相信大家都可以搭建出这个页面,下面是我搭建的页面模板代码,大家可以复制这个模板,只需实现里面的增删改查的业务逻辑即可。

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Supabase Demo</title>
    <link rel="stylesheet" href="https://unpkg.com/element-plus/dist/index.css">
    <!-- dayjs -->
    <script src="https://unpkg.com/dayjs@1.11.13/dayjs.min.js"></script>
    <!-- vuejs -->
    <script src="https://unpkg.com/vue@3"></script>
    <!-- supabase -->
    <script src="https://unpkg.com/@supabase/supabase-js@2"></script>
    <!-- element-plus -->
    <script src="https://unpkg.com/element-plus"></script>
    <!-- element-plus 中文包 -->
    <script src="//unpkg.com/element-plus/dist/locale/zh-cn"></script>
</head>

<body>
    <div id="app">
        <el-card>
            <!-- 搜索表单 -->
            <el-form :inline="true" :model="formInline" class="demo-form-inline">
                <el-form-item label="用户名">
                    <el-input v-model="formInline.username" placeholder="请输入用户名" clearable style="width: 220px;" />
                </el-form-item>
                <el-form-item label="性别">
                    <el-select v-model="formInline.gender" placeholder="请选择性别" clearable style="width: 220px;">
                        <el-option label="男" value="男"></el-option>
                        <el-option label="女" value="女"></el-option>
                    </el-select>
                </el-form-item>
                <el-form-item label="电话">
                    <el-input v-model="formInline.tel" placeholder="请输入电话" clearable style="width: 220px;" />
                </el-form-item>
                <el-form-item label="邮箱">
                    <el-input v-model="formInline.email" placeholder="请输入邮箱" clearable style="width: 220px;" />
                </el-form-item>
                <el-form-item>
                    <el-button type="primary" @click="getUserList">查询</el-button>
                    <el-button @click="resetQuery">重置</el-button>
                </el-form-item>
            </el-form>
            <div style="margin-bottom: 16px;">
                <el-button type="primary" @click="handleCreate">创建</el-button>
            </div>
            <!-- 表格 -->
            <el-table :data="tableData" v-loading="loading" style="width: 100%">
                <el-table-column prop="id" label="ID" width="80"></el-table-column>
                <el-table-column prop="avatar" label="头像" width="80">
                    <template #default="scope">
                        <el-avatar :size="40" :src="scope.row.avatar" />
                    </template>
                </el-table-column>
                <el-table-column prop="username" label="用户名" width="100"></el-table-column>
                <el-table-column prop="email" label="邮箱"></el-table-column>
                <el-table-column prop="password" label="密码"></el-table-column>
                <el-table-column prop="gender" label="性别" width="80"></el-table-column>
                <el-table-column prop="tel" label="电话"></el-table-column>
                <el-table-column prop="created_at" label="创建时间" width="180">
                    <template #default="scope">
                        {{ format(scope.row.created_at) }}
                    </template>
                </el-table-column>
                <el-table-column fixed="right" label="操作" width="120">
                    <template #default="scope">
                        <el-button link type="primary" size="small" @click="handleEdit(scope.$index, scope.row)">
                            编辑
                        </el-button>
                        <el-popconfirm title="您确认要删除这条数据吗?" @confirm="handleDelete(scope.row.id)">
                            <template #reference>
                                <el-button link type="danger" size="small">删除</el-button>
                            </template>
                        </el-popconfirm>
                    </template>
                </el-table-column>
            </el-table>
            <!-- 分页 -->
            <div style="display: flex; justify-content: flex-end; margin-top: 16px;">
                <el-pagination v-model:current-page="currentPage" v-model:page-size="pageSize"
                    :page-sizes="[10, 20, 50, 100]" layout="total, sizes, prev, pager, next, jumper" :background="true"
                    :total="totalCount" @size-change="handleSizeChange" @current-change="handleCurrentChange" />
            </div>

            <!-- 弹窗 -->
            <el-dialog destroy-on-close v-model="dialogVisible" :title="actionType === 'add' ? '创建' : '编辑'" width="500"
                @closed="handleClosed">
                <div v-if="actionType === 'add'"
                    style="display: flex; justify-content: space-between; align-items: center; color:rgb(103, 194, 58); background-color: rgb(240, 249, 235); padding: 8px 16px; border-radius: 4px;">
                    快速生成随机数据
                    <el-button @click="createData">生成</el-button>
                </div>

                <el-form :model="form" label-width="60px" style="margin-top: 16px;">
                    <el-form-item label="用户名">
                        <el-input v-model="form.username" autocomplete="off" placeholder="请输入"></el-input>
                    </el-form-item>
                    <el-form-item label="密码">
                        <el-input v-model="form.password" autocomplete="off" placeholder="请输入"></el-input>
                    </el-form-item>
                    <el-form-item label="邮箱">
                        <el-input v-model="form.email" autocomplete="off" placeholder="请输入"></el-input>
                    </el-form-item>
                    <el-form-item label="电话">
                        <el-input v-model="form.tel" autocomplete="off" placeholder="请输入"></el-input>
                    </el-form-item>
                    <el-form-item label="头像">
                        <el-input v-model="form.avatar" autocomplete="off" placeholder="请输入"></el-input>
                    </el-form-item>
                    <el-form-item label="性别">
                        <el-select v-model="form.gender" placeholder="选择性别">
                            <el-option label="男" value="男"></el-option>
                            <el-option label="女" value="女"></el-option>
                        </el-select>
                    </el-form-item>
                </el-form>
                <template #footer>
                    <div class="dialog-footer">
                        <el-button @click="dialogVisible = false">取消</el-button>
                        <el-button type="primary" @click="handleSubmit" :loading="submitLoading">
                            确定
                        </el-button>
                    </div>
                </template>
            </el-dialog>
        </el-card>
    </div>
</body>

<script type="module">
    import { Faker, zh_CN, en } from 'https://esm.sh/@faker-js/faker';
    const faker = new Faker({
        // 设置语言环境
        locale: [zh_CN, en],
    });

    const { createApp, ref, reactive, onMounted } = Vue;

    const app = createApp({
        setup() {
            const loading = ref(false);
            const submitLoading = ref(false);
            const currentPage = ref(1);
            const pageSize = ref(10);
            const totalCount = ref(0);
            const tableData = ref([]);
            const dialogVisible = ref(false);
            const actionType = ref('add');
            const editRow = ref(null);

            const formInline = reactive({
                username: '',
                gender: '',
                tel: '',
                email: '',
            })
            const form = reactive({
                username: '',
                password: '',
                email: '',
                tel: '',
                avatar: '',
                gender: '',
            });

            // 获取列表数据
            const getUserList = async () => {
                // ...
            }

            onMounted(() => {
                getUserList();
            });

            const format = (time) => {
                return dayjs(time).format('YYYY/MM/DD HH:mm:ss')
            }

            // 每页数量变化
            const handleSizeChange = (val) => {
                pageSize.value = val;
                getUserList();
            }

            // 当前页码变化
            const handleCurrentChange = (val) => {
                currentPage.value = val;
                getUserList();
            }

            const handleCreate = () => {
                actionType.value = 'add';
                dialogVisible.value = true;
            }

            const resetForm = () => {
                for (let key in form) {
                    form[key] = ""
                }
            }

            // 提交数据
            const handleSubmit = async () => {
                // ...
            }

            const createData = () => {
                form.username = faker.person.fullName()
                form.password = faker.internet.password()
                form.email = faker.internet.email()
                form.tel = faker.phone.number()
                form.avatar = faker.image.avatarGitHub()
                form.gender = faker.person.sex()
            }

            // 处理编辑
            const handleEdit = (index, row) => {
                editRow.value = row;

                actionType.value = 'edit';
                for (let key in form) {
                    form[key] = row[key];
                }
                dialogVisible.value = true;
            }

            // 处理删除
            const handleDelete = async (id) => {
                // ...
            }

            const handleClosed = () => {
                resetForm();
            }

            const resetQuery = () => {
                for (let key in formInline) {
                    formInline[key] = ''
                }
                getUserList();
            }

            return {
                loading,
                submitLoading,
                currentPage,
                pageSize,
                totalCount,
                tableData,
                format,
                handleSizeChange,
                handleCurrentChange,
                handleCreate,
                dialogVisible,
                handleSubmit,
                formInline,
                form,
                createData,
                handleDelete,
                handleEdit,
                handleClosed,
                actionType,
                getUserList,
                resetQuery,
            }
        }
    });

    app.use(ElementPlus, {
        locale: ElementPlusLocaleZhCn,
    });
    app.mount('#app');
</script>

</html>

模板预览 image.png

使用 Faker.js 来生成模拟数据

Faker.js 是一个用于生成假数据的 JavaScript 库,它可以帮助开发者在开发和测试过程中快速创建模拟数据。这个库非常适用于需要大量虚构信息(如名字、地址、电子邮件等)的应用场景,使得测试更加真实和有效。

以下是 Faker.js 的一些主要特点:

  1. 多种语言支持:Faker.js 支持多种语言的数据生成,包括但不限于英语、中文、日语、法语等。
  2. 丰富的数据类型:它可以生成各种类型的假数据,包括姓名、地址、电话号码、电子邮件、URL、颜色、日期时间、商品信息等。
  3. 易于集成:Faker.js 可以轻松地集成到 Node.js 环境中,也可以通过 CDN 在浏览器环境中使用。
  4. 可扩展性:用户可以根据自己的需求添加自定义的数据生成器或修改现有模块的行为。
  5. 社区活跃:作为开源项目,Faker.js 拥有一个活跃的社区,不断有新的功能被添加进来,并且问题得到及时解决。

简单使用:

<script type="module">
    import { Faker, zh_CN, en } from 'https://esm.sh/@faker-js/faker';
    const faker = new Faker({
        // 设置语言环境
        locale: [zh_CN, en],
    });
    console.log(faker.person.fullName()); // 随机姓名
    console.log(faker.internet.username()); // 随机账户
    console.log(faker.internet.password()); // 随机密码
    console.log(faker.person.sex()); // 随机性别
    console.log(faker.image.avatarGitHub()); // 随机头像
    console.log(faker.internet.email()); // 随机邮箱
    console.log(faker.phone.number()); // 随机手机号
</script>

连接 Supabase 数据库,实现增删改查

首先我们需要连接 Supabase 数据库:

image.png

// 连接数据库
const { createClient } = supabase;
const projectURL = "https://xxxxxxx.supabase.co";
const publicAnonKey = "your-anon-key";
const supabaseClient = createClient(projectURL, publicAnonKey);

实现查询

const getUserList = async () => {
    loading.value = true;
    // 计算开始索引
    const start = (currentPage.value - 1) * pageSize.value;
    // 计算结束索引
    const end = start + (pageSize.value - 1);
    let sqlQuery = supabaseClient.from('users')
        .select('*', { count: 'exact' })
        .order('id', { ascending: true })
        .range(start, end);

    if (formInline.username) {
        sqlQuery = sqlQuery.like('username', `%${formInline.username}%`)
    }
    if (formInline.gender) {
        sqlQuery = sqlQuery.eq('gender', formInline.gender)
    }
    if (formInline.tel) {
        sqlQuery = sqlQuery.eq('tel', formInline.tel)
    }
    if (formInline.email) {
        sqlQuery = sqlQuery.eq('email', formInline.email)
    }

    const { data, count, error } = await sqlQuery
    if (error) {
        ElementPlus.ElMessage({
            message: `查询失败,${error.message}`,
            type: 'error',
            plain: true,
        });
        loading.value = false;
        return;
    }

    totalCount.value = count;
    tableData.value = data;
    loading.value = false;
}
  • 通过 count: 'exact',获取列表的总条数
  • order('id', { ascending: true }),根据 id 字段升序排序
  • range(start, end),实现分页功能
  • sqlQuery.like('username', '%username%'),根据 username 字段进行模糊查询
  • sqlQuery.eq('gender', formInline.gender),根据 gender 字段精确匹配查询

实现新增和编辑

向数据库插入一条数据:

await supabaseClient.from('users').insert(form)

根据 id 字段更新数据:

await supabaseClient.from('users').update(form).eq('id', editRow.value.id);

实现 handleSubmit

const handleSubmit = async () => {
    try {
        submitLoading.value = true;
        let res;
        // 插入数据
        if (actionType.value === 'add') {
            res = await supabaseClient.from('users').insert(form);
        } else if (actionType.value === 'edit') {
            // update 更新数据
            res = await supabaseClient.from('users').update(form).eq('id', editRow.value.id);
        }

        if (res.error) {
            console.log('@@@_handleSubmit_error:', res.error);

            ElementPlus.ElMessage({
                message: `${actionType.value === 'add' ? '创建' : '编辑'}失败,请稍后重试`,
                type: 'error',
                plain: true,
            })
            return;
        }
        ElementPlus.ElMessage({
            message: `${actionType.value === 'add' ? '创建' : '编辑'}成功`,
            type: 'success',
            plain: true,
        })
        dialogVisible.value = false;
        getUserList();
    } catch (error) {
        console.error(error);
    } finally {
        submitLoading.value = false;
    }
}

实现删除

根据 id 字段删除数据:

await supabaseClient.from('users').delete().eq('id', id);

实现 handleDelete

const handleDelete = async (id) => {
    const { error } = await supabaseClient.from('users').delete().eq('id', id);
    if (error) {
        ElementPlus.ElMessage({
            message: '删除失败',
            type: 'error',
            plain: true,
        });
        return;
    }
    ElementPlus.ElMessage({
        message: '删除成功',
        type: 'success',
        plain: true,
    });
    getUserList();
}

总结

通过本文的学习,你应该已经掌握了如何使用 Supabase 数据库来构建现代 Web 应用的基础知识。Supabase 提供了一套强大且易于使用的工具,可以帮助开发者更高效地完成项目。希望本文对你有所帮助,祝你在 Supabase 的世界里探索愉快!以上就是本篇关于 Supabase 数据库的快速入门指南。如果你有任何问题或建议,欢迎在评论区留言交流。