探索 strapi.js - 使用 strapi 和 svelte 构建一个 Instagram 克隆版

2025-06-04

探索 strapi.js - 使用 strapi 和 svelte 构建一个 Instagram 克隆版

大家好,DEV 的各位好!顺便感谢你们的 50 位粉丝,你们太棒了!我又回来为大家带来新教程了,今天要介绍的是Strapi

Strapi 是一个 CMS(内容管理系统),类似于 Wordpress,但 Strapi 是无头架构的,这意味着它拥有丰富的 API,允许你使用它的功能,而无需受限于其前端。本质上,Strapi 是一个我们自己托管的 BaaS(后端即服务)。我们无需编写任何后端代码即可使用 Strapi。我们只需将它安装在我们的机器上即可。

所以,今天我将向你展示如何使用 Strapi 作为后端,并使用任何你想要的框架(我选择了 Svelte)作为前端,来创建一个Instagram克隆版。你可以在这里查看实时应用。项目的源代码可以在这里获取。

创建 Strapi 项目

如果您还记得,在我上一篇教程中,当我向您展示使用Firebase的无服务器时,我们必须注册 Firebase,创建一个项目,创建一个应用程序,最后将 Firebase 配置添加到我们的项目中。

这次就简单多了。你需要安装NodeJS 和 NPM,希望你们都已经安装好了。要创建 strapi 应用,我们执行以下命令:

# cms is the name of the folder that strapi will be in
npx create-strapi-app cms --quickstart
cd cms
npm run develop
Enter fullscreen mode Exit fullscreen mode

让你想起了create-react-app,不是吗?

这将在文件夹中创建一个 Strapi 应用程序cms,请转到该文件夹​​并启动该应用程序。您可以让此终端窗口在后台运行。

--quickstart选项使我们更容易使用 SQLite 作为数据库来运行应用程序。

创建我们的前端应用程序

现在让我们专注于前端。我将使用Svelte作为前端,因为我已经爱上了它。Svelte 易于理解,因此你可以将其转换为你正在使用的框架。我还将使用 TypeScript 作为开发语言,而不是 JavaScript,所以要小心,如果你要使用 JavaScript,请删除所有类型断言、接口之类的东西。

要创建 svelte 应用程序,我们可以使用 degit。

npx degit sveltejs/template frontend
cd frontend
# Open in VSCode
code .
# Make app into typescript
node scripts/setupTypescript.js
npm i
Enter fullscreen mode Exit fullscreen mode

在设置前端的同时,我们还添加一个像 Page.js 这样的路由器来制作 SPA 应用程序。

npm i page
# for typescript only
npm i -D @types/page
Enter fullscreen mode Exit fullscreen mode

前端的基本样板

现在,我们来添加基本的样板代码。比如添加样式、首页 UI 以及添加路由器。

<!-- public/index.html -->

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset='utf-8'>
    <meta name='viewport' content='width=device-width,initial-scale=1'>

    <title>Quickstagram - Instagram, but quick!</title>

    <link rel='icon' type='image/png' href='/favicon.png'>
    <link rel='stylesheet' href='/global.css'>
    <link rel="stylesheet" href="https://www.w3schools.com/w3css/4/w3.css">
    <link href="https://use.fontawesome.com/releases/v5.0.1/css/all.css" rel="stylesheet">
    <link rel='stylesheet' href='/build/bundle.css'>

    <script defer src='/build/bundle.js'></script>
</head>
<body>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode
<!-- src/App.svelte -->

<script lang="ts">
    import { setContext } from "svelte";
    import router from "page";
    import { parse } from "qs";
    import Index from "./routes/index.svelte";
    import Navbar from "./components/Navbar.svelte";

    export let strapiApiUrl: string;

    let page;
    let params;
    let queryString;

    function setupRouteParams(ctx: PageJS.Context, next) {
        params = ctx.params;
        queryString = parse(ctx.querystring);
        next();
    }

    router("/", setupRouteParams, () => (page = Index));

    router.start();

    setContext("apiUrl", strapiApiUrl);
</script>

<Navbar />

<!-- This component just renders the component `this`. It is used to render components dynamically, like how we're doing -->
<svelte:component this={page} {params} {queryString} />
Enter fullscreen mode Exit fullscreen mode
<!-- src/components/Index.svelte -->

<script lang="ts">
    import Auth from "../components/Auth.svelte";

    export const queryString = {};
    export const params = {};
</script>

<div class="w3-container">
    <h1 class="w3-center w3-xxxlarge">Quickstagram</h1>
    <p class="w3-center w3-large w3-text-gray">Instagram, but quicker!</p>

    <div class="w3-center">
        <a
            href="/auth?action=register"
            class="w3-button w3-blue w3-border w3-border-blue w3-hover-blue">Register</a>
        <a
            href="/auth?action=login"
            class="w3-button w3-white w3-border w3-border-black w3-hover-white">Login</a>
    </div>

    <Auth />
</div>
Enter fullscreen mode Exit fullscreen mode
<!-- src/components/Navbar.svelte -->

<script lang="ts">
    import { slide } from "svelte/transition";
    let active = false;
</script>

<style>
    .toggler {
        display: none;
    }

    @media (max-width: 600px) {
        .logo {
            display: block;
            width: 100%;
        }
        .logo .toggler {
            float: right;
            display: initial;
        }
        .nav {
            display: flex;
            width: 100%;
            flex-direction: column;
        }

        .nav a {
            text-align: left;
        }
    }
</style>

<div class="w3-bar w3-blue">
    <div class="logo">
        <a
            href="/"
            class="w3-bar-item w3-text-white w3-button w3-hover-blue">Quickstagram</a>
        <button
            class="toggler w3-button w3-blue w3-hover-blue"
            on:click={() => (active = !active)}>
            <i class="fas fa-{active ? 'times' : 'bars'}" /></button>
    </div>
    <div class="w3-right w3-hide-small">
        <a href="/upload" class="w3-bar-item w3-button w3-hover-blue">Upload</a>
        <a
            href="/auth?action=login"
            class="w3-bar-item w3-button w3-hover-blue">Login</a>
        <a
            href="/auth?action=register"
            class="w3-bar-item w3-button w3-purple w3-hover-purple">Register</a>
    </div>
    {#if active}
        <div class="w3-right nav w3-hide-large w3-hide-medium" transition:slide>
            <a
                href="/upload"
                class="w3-bar-item w3-button w3-hover-blue">Upload</a>
            <a
                href="/auth?action=login"
                class="w3-bar-item w3-button w3-hover-blue">Login</a>
            <a
                href="/auth?action=register"
                class="w3-bar-item w3-button w3-purple w3-hover-purple">Register</a>
        </div>
    {/if}
</div>
Enter fullscreen mode Exit fullscreen mode
<!-- src/components/ErrorAlert.svelte -->

<script lang="ts">
    export let message: string;
</script>

<div
    class="w3-panel w3-pale-red w3-leftbar w3-border-red w3-text-red w3-padding">
    {message}
</div>
Enter fullscreen mode Exit fullscreen mode
src/components/Auth.svelte

<script lang="ts">
    import Error from "./ErrorAlert.svelte";
    import { fade } from "svelte/transition";

    type AuthMode = "login" | "register";

    export let authMode: AuthMode = "register";

    let loginError: string | null = null;
    let registerError: string | null = null;

    let email = "";
    let password = "";
    let cpassword = "";
    let username = "";

    function login() {
        email = email.trim();
        password = password.trim();

        if (!email || !password) {
            loginError = "Fill out all fields!";
            return;
        }
        loginError = null;
    }

    function register() {
        email = email.trim();
        password = password.trim();
        cpassword = cpassword.trim();
        username = username.trim();

        if (!email || !password || !cpassword || !username) {
            registerError = "Fill out all fields!";
            return;
        }
        registerError = null;
    }
</script>

<style>
    .auth-box {
        width: 40%;
        margin: 1rem auto;
    }

    @media (max-width: 600px) {
        .auth-box {
            width: 80%;
        }
    }
</style>

<div class="w3-container">
    <div class="w3-card-4 w3-border w3-border-black auth-box">
        <div class="w3-bar w3-border-bottom w3-border-gray">
            <button
                style="width: 50%"
                on:click={() => (authMode = 'login')}
                class="w3-bar-item w3-button w3-{authMode === 'login' ? 'blue' : 'white'} w3-hover-{authMode === 'login' ? 'blue' : 'light-gray'}">Login</button>
            <button
                style="width: 50%"
                on:click={() => (authMode = 'register')}
                class="w3-bar-item w3-button w3-{authMode === 'register' ? 'blue' : 'white'} w3-hover-{authMode === 'register' ? 'blue' : 'light-gray'}">Register</button>
        </div>
        <div class="w3-container">
            <h3>{authMode === 'login' ? 'Login' : 'Register'}</h3>

            {#if authMode === 'login'}
                <form on:submit|preventDefault={login} in:fade>
                    {#if loginError}
                        <Error message={loginError} />
                    {/if}
                    <div class="w3-section">
                        <label for="email">Email</label>
                        <input
                            type="email"
                            bind:value={email}
                            placeholder="Enter your email"
                            id="email"
                            class="w3-input w3-border w3-border-black" />
                    </div>
                    <div class="w3-section">
                        <label for="password">Password</label>
                        <input
                            type="password"
                            bind:value={password}
                            placeholder="Enter your password"
                            id="password"
                            class="w3-input w3-border w3-border-black" />
                    </div>
                    <div class="w3-section">
                        <button
                            class="w3-button w3-blue w3-hover-blue w3-border w3-border-blue">Login</button>
                        <button
                            class="w3-button w3-white w3-hover-light-gray w3-border w3-border-black"
                            on:click={() => (authMode = 'register')}>Register</button>
                    </div>
                </form>
            {:else}
                <form on:submit|preventDefault={register} in:fade>
                    {#if registerError}
                        <Error message={registerError} />
                    {/if}
                    <div class="w3-section">
                        <label for="username">Username</label>
                        <input
                            type="text"
                            bind:value={username}
                            placeholder="Enter a username"
                            id="username"
                            class="w3-input w3-border w3-border-black" />
                    </div>
                    <div class="w3-section">
                        <label for="email">Email</label>
                        <input
                            type="email"
                            bind:value={email}
                            placeholder="Enter your email"
                            id="email"
                            class="w3-input w3-border w3-border-black" />
                    </div>
                    <div class="w3-section">
                        <label for="password">Password</label>
                        <input
                            type="password"
                            bind:value={password}
                            placeholder="Enter a password"
                            id="password"
                            class="w3-input w3-border w3-border-black" />
                    </div>
                    <div class="w3-section">
                        <label for="cpassword">Confirm Password</label>
                        <input
                            type="password"
                            bind:value={cpassword}
                            placeholder="Re-enter that password"
                            id="cpassword"
                            class="w3-input w3-border w3-border-black" />
                    </div>
                    <div class="w3-section">
                        <button
                            class="w3-button w3-blue w3-hover-blue w3-border w3-border-blue">Register</button>
                        <button
                            class="w3-button w3-white w3-hover-light-gray w3-border w3-border-black"
                            on:click={() => (authMode = 'login')}>Login</button>
                    </div>
                </form>
            {/if}
        </div>
    </div>
</div>
Enter fullscreen mode Exit fullscreen mode

现在,我们还添加一条/auth仅呈现我们的身份验证组件的路由。

<!-- src/routes/auth.svelte -->

<script lang="ts">
    import Auth from "../components/Auth.svelte";
    import router from "page";

    export const params = {};
    export let queryString: { action: "login" | "register"; next: string };
</script>

<Auth authMode={queryString.action} on:auth={() => router.redirect(queryString.next)} />
Enter fullscreen mode Exit fullscreen mode

我们需要auth.svelte在路由器中注册这个组件,所以让我们这样做:

<!-- src/App.svelte -->

<script lang="ts">
    // ...
    import Auth from "./routes/auth.svelte";
    // ...

    router("/", setupRouteParams, () => (page = Index));
    router("/auth", setupRouteParams, () => (page = Auth));

    // ...
</script>

<!-- ... -->
Enter fullscreen mode Exit fullscreen mode

您可能会注意到路由不起作用。这是因为我们的应用尚未配置为 SPA 兼容。让我们来配置一下。编辑您的package.json

"scripts": {
    "build": "rollup -c",
    "dev": "rollup -c -w",
    "start": "sirv public -s --host",
    "validate": "svelte-check"
}
Enter fullscreen mode Exit fullscreen mode

使用 重新运行您的应用程序npm run dev

配置 strapi

由于我们使用的是 SQL 数据库,因此需要为数据库创建模型。幸运的是,Strapi 让这一切变得简单。前往 Strapi 管理面板,地址是localhost:1337/admin

确保你的服务器仍在运行!如果没有,请使用以下命令运行它:

npm run develop

创建帖子模式

让我们定义一下我们的帖子,也就是一篇帖子应该包含哪些内容。由于这是一个 Instagram 的克隆版本,我们可以问“一篇 Instagram 帖子应该包含哪些内容?”它包含:

  • 帖子图片
  • 帖子作者
  • 帖子内容
  • 该帖子的点赞数
  • 该帖子的评论
  • 帖子创建时间

让我们post在 Strapi 中创建一个收藏夹。请按照以下视频中的步骤操作:

在上面的视频中,我创建了一个created列。这不是必需的,因为 Strapi 会自动执行此操作。

现在,我们需要评论。让我们创建一个comment集合。

我们还可以为字段(在 SQL 中称为列)设置某些属性。

现在,说到 SQL 的卖点——关系。让我们添加名为“关系”的特殊列,这样我们就可以使用该字段引用另一个表

大功告成!现在让我们访问 Strapi API!

访问 API

我们需要一个像InsomniaPostman这样的程序来发出 API 请求我选择前者)。如果你使用的是 MacOS/Linux 这样的 *nix 系统,你也可以使用 cURL 命令。

未经身份验证的请求

任何人都应该能够在无需登录的情况下访问我们 API 的部分功能,例如,他们可以访问帖子、图片和评论,但不能删除或上传它们。让我们尝试访问帖子。要从 API 访问任何集合,端点如下:

GET http://localhost:1337/<collection_name>
Enter fullscreen mode Exit fullscreen mode

要获取任何端点的集合名称:

太棒了!现在我们有了集合名称,让我们调用端点http://localhost:1337/posts

获取帖子(失眠)

什么!我们收到403 FORBIDDEN错误。为什么?这是因为我们还没有设置任何规则。规则决定了谁可以看到什么。让我们将postcomment的规则改为:

现在,如果我们重新运行 API 请求:

获取帖子(工作版本)(失眠)

我们可以看到它起作用了!

cURL 中也是一样:

$ curl -X GET http://localhost:1337/posts
[]
Enter fullscreen mode Exit fullscreen mode

我们也可以对评论做同样的事情:

替代文本

经过身份验证的请求

现在,让我们关注已通过身份验证的用户。经过身份验证的用户应该能够执行与未经身份验证的用户相同的操作,但他们还可以创建帖子和评论。如果我们尝试通过向 发送POST请求来创建评论http://localhost:1337/comments,则会收到403 FORBIDDEN错误。

替代文本

记得将Content-Typeheader 设置为application/json

与 cURL 相同

$ curl -X POST -H "Content-Type: application/json" -d '{"content": "Hello world!"}' http://localhost:1337/comments
{"statusCode":403,"error":"Forbidden","message":"Forbidden"}
Enter fullscreen mode Exit fullscreen mode

让我们通过身份验证来解决这个问题。身份验证后,我们会收到一个JSON Web Token,然后可以使用Authorization 标头将其附加到其他请求中。要进行身份验证,我们需要向 发送POST请求http://localhost:1337/auth/local。此POST请求应包含一个identifier字段(可以是用户的电子邮件地址或用户名)和一个password字段(即用户的密码)。

我们没有任何用户。让我们注册一个:

替代文本

您可以看到我们返回了一个token。这token就是我之前提到的 JSON Web Token。让我们在之前的请求中使用它来创建一个新的帖子:

$ curl -X POST -H "Content-Type: application/json" -H "Authorization: Bearer paste_your_token_here" -d '{"content": "Hello, world!"}' http://localhost:1337/comments
{"id":1,"content":"Hello, world!","user":null,"post":null,"published_at":"2020-11-11T07:07:26.552Z","created_at":"2020-11-11T07:07:26.564Z","updated_at":"2020-11-11T07:07:26.564Z"}
Enter fullscreen mode Exit fullscreen mode

评论已成功添加!恭喜!让我们看看 Strapi 管理面板。我们可以看到我们的更改已经生效了!

替代文本

如果您的令牌过期POST,您可以通过向 发送 >请求来重新登录/auth/local。例如:

curl -X POST -H "Content-Type: application/json" -d '{"identifier": "your email", "password": "your password"}' http://localhost:1337/auth/local

结论

以上就是对 strapi.js 的介绍。这是本系列的第一部分。下一部分,我们将深入探讨前端和其他精彩内容!

查看第二部分

文章来源:https://dev.to/arnu515/exploring-strapi-js-build-an-instagram-clone-with-strapi-and-svelte-35i6
PREV
async/await:底层原理
NEXT
How NOT to follow your passion 😬 1. Are you passionate about the THING? 🐴 Or are you passionate about the IDEA? 🦄 2. Why PASSION is not HAPPINESS 😞 - and what to do about it 😇 3. Your PASSION is your STRENGTH 🚀 How to turn your PASSION into HAPPINESS 💐 EXTRA value 🍓