为了账号安全,请及时绑定邮箱和手机立即绑定

用Encore.ts和Next.js搭建个人博客平台

标签:
JavaScript

在这次教程里,我们将构建一个实用且有趣的项目:“一个个人博客平台”。我们将专注于使用Encore.ts来构建后端,使用REST API。

我将一步步从头开始,教你如何在Encore Cloud的免费托管平台上部署后端服务。我们将使用PostgreSQL数据库来存储我们的博客数据。

激动不激动?那咱们就开始吧!

Encore 是什么呢?

图片说明:这是一张有趣的图片

Encore 是一个开源的后端框架,用于构建可扩展的分布式系统。它是一个面向开发者的工具,借助其高性能的 API,可以轻松构建健壮且类型安全的应用。无论你是 TypeScript 还是 Go 的粉丝,Encore 都能支持。

encore 还自带了一些开发工具,不论是小型个人项目还是大型后端系统,都能助你一臂之力。对于追求简洁、性能和扩展性的开发者来说,它是一个很好的选择。

需要满足的条件

你不需要成为Encore专家才能跟随这个教程。但是既然我们在构建一个REST API,如果你对REST API的工作原理有一些基本了解会更好。

GET, POST, PUT, DELETE 这些常用的请求方法在我们继续学习的过程中会很有帮助。

如果你之前只是稍微接触过 Node.js 或 Express,这也非常有帮助。知道如何设置服务器路由或搭建一个基本的服务器将帮助你更好地理解相关内容。

在我们开始之前,请确保你已经安装了最新版本的 Node.js。如果没有安装,请访问Node.js 官网进行下载。一旦搞定,我们就可以开始了。

图片 点击图片查看

在我们开始前,还有一件事:Encore CLI。访问 encore.dev 网站,你可以找到所有所需的安装信息。请根据你的操作系统(Windows、macOS 或 Linux),按照对应的说明进行操作。

您可以复制粘贴以下命令到终端直接安装。


    # 使用 Homebrew 在 macOS 上安装 Encore
    brew install encoredev/tap/encore

    # 在 Windows (Powershell 环境) 中安装 Encore
    iwr https://encore.dev/install.ps1 | iex  # 使用 iwr 命令下载安装脚本 | 使用 iex 命令执行下载的脚本

    # 使用 curl 命令下载并执行安装脚本,在 Linux 上安装 Encore
    curl -L https://encore.dev/install.sh | bash

全屏 退出全屏

现在你已经安装好了一切,让我们确认一下Node.js和Encore是否已经在你的机器上安装好了。就像是你要开始一个新的项目,确认你所有的工具都已经准备好了。

这里是如何检查Node.js版本的方法。

运行 node -v 命令

切换到全屏模式;退出全屏

这会显示你安装的 Node.js 版本。如果它显示版本号,说明安装成功了。

我们来看看安可部分吧:

encore版

全屏模式 退出全屏

如果 Encore 安装正确的话,它也会显示出它的版本。

看到两个版本号了没?太好了。那就太好了,我们可以继续咯。如果没有,请再检查一下安装步骤,我们来搞定它。

图片

设置项目

现在我们来创建Encore项目(Encore项目)。只需运行这个命令,它会一步步教你设置Encore项目。

要创建encore项目,你可以使用以下命令:

创建 encore app

点按全屏 点按退出全屏

当你运行这个时,Encore 会要求你为项目选择一种语言。在本教程中,我们将选择 TypeScript,因为它是一个构建强大和可扩展 API 的绝佳选择。

请使用键盘上的方向键选择 TypeScript,然后按回车键。

图片

当你运行命令 encore app create 时,Encore 会提供几个模板供你选择。这些就像是预设的蓝图,每个都能帮助你根据项目类型快速上手。

但在这个教程里,我们要从最简单的开始,从零开始。我们选择“Empty app”模板。

为什么?因为从头开始建造是了解所有东西是怎么运转的最佳方法。

所以,去选择 空应用 模板。Encore 会给你一个干净的空项目。

一旦你完成这一步,Encore 会为你生成一个简洁明了的项目结构。

没有任何额外的代码,也没有预建的功能,只有一块等待你的创意的空白画布。

图片
这是一张图片。

现在到了有趣的部分,给项目取个名字。在本教程中,我将项目命名为“blogs”,但你可以选择你觉得有意义的名字。

如图所示

一旦你按下Enter键,Encore会自动立即启动。它会自动开始下载“空应用”模板,并在后台自动为你设置项目。

你将在终端中看到大量活动,就像文件会陆续创建,依赖项会陆续安装,所有内容都被整齐地组织到一个清晰的项目结构中。

设置完成后,Encore 可能会提示你运行项目。它会建议你用 encore run 这样的命令启动项目。

但是,我们先别急着运行它。我们切换到你的代码编辑器,打开项目,看看项目的结构,并在那里运行它。

这样在我们构建和调整代码时,能更好地跟上进度。

图片 如图所示

现在让我们一起打开你在代码编辑器中的项目。接下来,我要给你展示一个功能,它会大大减轻你作为开发者的日常负担。

相信我,你会喜欢的,快进来!

图片 这是一张图片,看起来很有意思。

当你在代码编辑器中打开项目时,你会觉得它非常清爽简洁。没有杂乱的文件或元素,也没有让人眼花缭乱的依赖项列表。如果查看 package.json,你会只看到一个主要的依赖:encore.dev

就这样。TypeScript 作为开发依赖默默地待着,保持代码整洁和专注。

在你的代码编辑器中打开命令行并运行以下命令:

再来一遍吧

全屏 退出全屏

你一按下Enter,就会发生神奇的事情。Encore 不仅仅启动你的项目,它还会自动打开浏览器,并直接带你到开发人员仪表板

当我们开始打造我们的API时,我们将深入了解它,包括跟踪请求,排查错误等等。但现在,先花点时间来欣赏一下这一切是多么的顺畅。

Encore在承担重任,所以你可以专注于真正重要的事情:写出精彩的代码。

咱们接着来吧,还有好多好玩的等着我们呢!

图片

实现API接口功能

行了,让我们来创建第一个API端点。我们将创建一个简单的/hello端点,当你访问这个端点时,它会返回“Hello, world!”

让我们看看我们怎么能做到。现在,让我们回到代码编辑区。

图片
这是一张图片。

我在项目根目录下新建了一个叫hello的文件夹,并在里面加了一个叫hello.ts的文件。

我们可以通过组织文件夹和文件来保持项目有条理,让文件夹和文件结构随着项目的增长依然清晰合理。

hello.ts 文件里,我引入了 api 函数。这个函数是定义端点的关键,它主要涉及两部分内容。

  1. 一个作为第一个参数的对象,其中我们定义方法(如 GET、POST 等)、路径(如 /hello),以及一个名为 expose: true 的键。设置 expose: true 表示这个端点是公开的,任何人都可以访问它。
  2. 一个异步函数,其中编写端点应该返回的内容的逻辑。在这个例子中,它会返回一个简单的“Hello, world!”消息。

我希望这能让你对端点的结构有一个基本的了解,并知道在使用它们时需要注意些什么。

现在我们已经搞定基础知识,让我们切换一下方向,开始构建我们博客平台的REST接口。这里就变得有意思了!

写博客:

我们先在项目根目录下创建一个名为 blog 的新文件夹。在这个文件夹中,我们将添加一个名为 blog.ts 的文件。在这文件里,我们会定义所有与博客功能相关的内容,保持代码整洁有序。

首先,让我们为我们的博客定义博客的接口。由于我们正在构建一个简单的博客网站,我们会让事情尽可能简单。目前,我们只需要定义三个字段即可创建一个博客。

  • 标题: 博客标题。
  • 内容: 博客的主要内容。
  • 作者: 博主的名字。

我们这样用TypeScript来组织它

// 博客接口定义,包含博客的基本信息
interface Blog {
  id: number;
  title: string;
  content: string;
  author: string;
  created_at: string;
  updated_at: string;
}

// 创建博客参数的接口定义,仅包括创建博客时需要的参数
interface CreateBlogParams {
  title: string;
  content: string;
  author: string;
}

点击全屏/退出全屏

所以这里,我们现在定义了这个createBlog函数,这是一个用于发布的端点。它将根据接收到的博客信息创建一个博客。

    import { api } from "encore.dev/api";

    interface Blog {
      id: number;
      title: string;
      content: string;
      author: string;
      created_at: string;
      updated_at: string;
    }

    interface CreateBlogParams {
      title: string;
      content: string;
      author: string;
    }

    // 创建新博客
    export const createBlog = api<CreateBlogParams, Blog>(
      {
        method: "POST",
        path: "/blogs",
        expose: true,
      },
      async ({ title, content, author }: CreateBlogParams) => {
        return { title, content, author } as Blog;
      }
    );

切换到全屏 / 关闭全屏

让我们使用Encore提供的开发者仪表板。可以把它想象成超级版的Postman(或Koa),但功能多得多。打开仪表板后,你会发现一个下拉菜单,列出了所有的端点。

比如说,如果你创建了一个名为 createBlog 的端点,它就会出现在这个列表里。你创建的每个端点都会出现在这里边。

我们已经通过发送一些例如博客标题、内容和作者姓名的示例数据测试了“创建博客”端点,它工作得很好。

控制面板不仅显示你发送的请求,还显示你接收到的响应,非常直观易懂,让调试和测试更加简便,简化了整个过程。

图片 看这张图片

在我们深入探讨创建API之前,让我们先花点时间来设置数据库。毕竟,我们的博客网站需要一个地方来存放所有的博客文章,不是吗?

我们将使用PostgreSQL作为数据库,Encore让PostgreSQL的集成和管理变得非常简单。

一旦数据库设置完毕并准备就绪,我们将回到API创建部分。这样一来,我们不仅能够创建博客,还能无缝地存储和读取它们。

让我们先让数据库跑起来,然后再继续开发那些API接口!

连接到 PostgreSQL数据库

为了连接到PostgreSQL数据库,首先,你需要在你的机器上安装并运行Docker。Docker非常适合创建和管理像PostgreSQL这样的数据库,可以防止你的本地环境变得过于复杂。

别着急,如果没有 Docker 的话,只需去官网下载适合你操作系统的版本。

安装非常简单,你很快就能创建一个 PostgreSQL 实例环境。

有了 Docker 后,我们将运行 PostgreSQL 服务,然后连接到我们的项目。接下来,我们先获取 Docker 吧!如果需要,可以将“encore项目”改为“安可项目”或直接使用“encore项目”。

图片 如图所示,点击图片查看大图。

一旦Docker启动了,你暂时就不用再管它了。它只是在后台默默地干它的活。

现在,让我们专注于为我们的博客 API 创建一个 数据库架构。为此,我们将利用 迁移文件。迁移就像是数据库的版本控制系统,它们帮助你在时间推移中定义和更新数据库。

我们就这样配置它:

  1. 在我们之前创建的博客目录下,我们将添加一个名为 migrations 的目录。所有数据库迁移文件将存放在这里。
  2. migrations 文件夹内,我们将创建一个名为 1_create_blogs_table.up.sql 的文件。这个名字可能看起来有些特殊,但它只是一个约定,1 表示这是第一个迁移文件,而 .up.sql 表示这是一个用于创建或更新数据库的文件。可以用来创建或更新数据库模式结构。

在这份文件里,我们将定义blogs表的结构。

比如说:

    创建一个名为 blogs 的表 (
        id  自增 PRIMARY KEY,
        title TEXT NOT NULL,
        content TEXT NOT NULL,
        author TEXT NOT NULL,
        created_at TIMESTAMP 默认当前时间戳,
        updated_at TIMESTAMP 默认当前时间戳
    );

全屏查看,退出全屏

此结构包括:

  • 一个自动递增的 id 列,用于标识每篇新博客文章。
  • 用于存储博客标题、内容和作者信息的 titlecontentauthor 列。
  • 一个 created_at 列,记录博客创建时间。

一旦这个迁移文件准备好了,Encore就会在我们运行项目时自动将其应用到数据库。

就这样,我们的数据库就可以存储博客文章了!

    import { SQLDatabase } from "encore.dev/storage/sqldb"; // 导入SQL数据库模块

全屏 退出全屏

现在,让我们把代码和数据库连接起来。为此,我们需要从Encore的sqldb模块中导入SQLDatabase。这将让我们能够无缝地与PostgreSQL数据库进行交互。

一旦我们导入它,我们就将创建一个数据库实例,这是一个具体的数据库运行环境。因为我们叫这个数据库blogs,我们就用这个名字来代表。

在数据库实例中,我们也需要指定 migrations 文件夹的路径。这告诉 Encore 去哪里找我们之前创建的迁移文件。

代码里看起来是这样的。

const db = new SQLDatabase("blogs", { migrations: "./migrations" }); // 创建一个名为 "blogs" 的SQL数据库实例,迁移路径为 "./migrations"

切换到全屏,恢复正常视图

通过这样的设置,Encore 可以找到迁移文件;当我们运行项目时,它会自动应用到 blogs 数据库。

这在我们开发博客平台时,处理数据库结构是一种干净有效的方式。

    import { api, APIError } from "encore.dev/api";
    import { SQLDatabase } from "encore.dev/storage/sqldb";

    const db = new SQLDatabase("blogs", { migrations: "./migrations" });

    interface Blog {
      id: number;
      title: string;
      content: string;
      author: string;
      created_at: string;
      updated_at: string;
    }

    interface CreateBlogParams {
      title: string;
      content: string;
      author: string;
    }

    // 创建一个新的博客
    export const createBlog = api<CreateBlogParams, Blog>(
      {
        method: "POST",
        path: "/blogs",
        expose: true,
      },
      async ({ title, content, author }: CreateBlogParams): Promise<Blog> => {
        const row = await db.queryRow<Blog>`
          INSERT INTO blogs (title, content, author)
          VALUES (${title}, ${content}, ${author})
          返回 id, title, content, author, created_at, updated_at`;

        if (!row) {
          throw APIError.internal("创建博客时发生错误");
        }
        return row;
      }
    );

全屏 退出

我们已经使用了一个SQL查询将 titlecontent 以及 author 这三项保存到了 blogs 数据库里。

这是一个简单的SQL命令,用于将数据存储在数据库中。当你重新运行应用程序时,你会注意到,Docker过程结束后,会自动为你创建一个数据库容器。

图片

好的,让我们来试试在开发者仪表板上测试我们这个端点。进入仪表板,你会在那里看到你的端点。

只需点击它,输入所需的数据,再点一下发送。你会马上看到回复,是成功还是出错。

这真是个快速又简单的方法,确保一切都按预期运行。我们试试看!

图片

现在,当我们检查返回时,可以看到idcreated_atupdated_at字段。这意味着我们的数据已经成功地保存到了数据库里。一切工作得很好!

如果你想更仔细地看看这些表格和数据,可以用Encore DB Shell这个工具。

这是一款很方便的工具,让你可以直接与数据库进行操作。

要启动 shell,只需在终端中简单地运行命令。

encore db shell blogs

全屏模式 退出全屏

这里,blogs 是我们使用的数据库名称。运行之后,你就会进入数据库 shell 界面,在那里你可以查看表,运行 SQL 查询,并清楚地了解数据库里存了些什么。

咱们试试看,瞧瞧底下藏着啥!

图片

我们现在可以查询这个表了。

图片

我们可以看到数据,虽然有点挤。但关键是它能用了!数据保存正确,我们也验证过了。

就这样,我们成功地完成了创建博客的第一个端点。很酷,对吧?

咱们继续加油吧,一起做得更好!

查看所有博客:

现在,我们来创建我们的第二个端点,这次是从我们数据库中获取所有博客。就像我们之前为createBlog端点所做的那样,我们也将遵循类似的步骤。

我们将定义端点接口,并编写逻辑来从数据库获取数据并将其作为响应返回。

这都是建立在我们已经学到的基础上的。让我们直接上手,把这事搞定吧!

    // 获取所有博客
    export const getBlogs = api(
      {
        method: "GET",
        path: "/blogs",
        expose: true,
      },
      async (): Promise<{ blogs: Blog[] }> => {
        const rows = db.query`
              SELECT id, title, content, created_at
              FROM blogs
              ORDER BY created_at DESC
          `;

        const blogs: Blog[] = [];
        for await (const row of rows) { // 对于每一行数据
          blogs.push({
            id: row.id,
            title: row.title,
            content: row.content,
            author: row.author, // 作者
            created_at: row.created_at,
            updated_at: row.updated_at,
          });
        }

        return { blogs };
      }
    );

点击此处进入全屏模式 点击此处退出全屏

我们来试试新端点,看看所有博客是不是都正常显示了。

转到 开发者仪表板,在下拉菜单中找到 getAllBlogs 端点,然后点击发送。

如果你的设置都正确,你应该能看到一个包含你数据库中所有博客的列表。

图片
这是一张图片。点击可以查看原图。

通过ID查找博客

现在,我们将创建一个端点来通过ID获取单个博客。为此,我们将使用id作为函数参数。然后,我们将编写一个简单的SQL查询来从数据库中获取与给定id匹配的博客内容。

很简单,只需要对咱们之前做的稍微改一下。咱们建起来试试,看看效果咋样。

    // 通过ID读取一个博客
    export const getBlogById = api<{ id: number }, Blog>(
      {
        method: "GET",
        path: "/blogs/:id",
        expose: true,
      },
      async ({ id }: { id: number }): Promise<Blog> => {
        const row = await db.queryRow<Blog>`
          SELECT * FROM blogs WHERE id = ${id}`;
        if (!row) {
          throw APIError.notFound("找不到这篇博客");
        }
        return row;
      }
    );

点击全屏观看 按钮退出全屏

让我们使用开发者仪表板来测试这个新的接口。目前数据库中只有一个博客文章,我们来检查这个id: 1的博客文章。

转到仪表板,找到 getBlogById 端点,将 ID 填为 1。点击发送按钮,你应该会在响应中看到这个博客的详细信息。

点击这里看大图

按ID更新博客帖子:

通过ID更新博客非常简单。首先,我们将定义一个 UpdateBlogParams 接口来规范我们期望的数据,比如 id标题内容作者

我们将使用一个 SQL 查询来根据提供的 id 更新数据库中的博客条目。

如果找不到对应 id 的博客,我们会显示一个错误消息让用户知道。

    interface UpdateBlogParams {
      id: number;
      标题: string;
      正文: string;
    }

    // 更新博客文章
    export const updateBlog = api<UpdateBlogParams, Blog>(
      {
        method: "PUT",
        path: "/blogs/:id",
        expose: true,
      },
      async ({ id, 标题, 正文 }: UpdateBlogParams): Promise<Blog> => {
        const row = await db.queryRow<Blog>`
          UPDATE 博客
          SET 标题 = ${标题}, 正文 = ${正文}, 最后更新时间 = CURRENT_TIMESTAMP
          WHERE id = ${id}
          返回 id, 标题, 正文, 作者, 创建时间, 最后更新时间`;

        if (!row) {
          throw APIError.notFound("找不到博客");
        }
        return row;
      }
    );

全屏显示 退出全屏

我们试试这个,看看效果如何。前往开发者仪表板,找到updateBlog端点,并传递你想要更新的博客的id,以及新的标题、内容或作者。

图片

titlecontent 字段已经根据我们发送的数据更新了 博客1

查看开发者仪表盘中的响应,你就会看到变化的反映。

根据ID删除博客:

现在,让我们看看如何通过其ID删除一个博客。我们将id作为函数的参数,并执行一个简单的SQL命令来从数据库中删除博客。

如果该博客存在,它将会被删除。如果不存在的话,我们将用错误消息妥善处理。

    // 删除博客的API
    export const deleteBlog = api<{ id: number }, { message: string }>(
      {
        method: "DELETE",
        path: "/blogs/:id",
        expose: true,
      },
      async ({ id }: { id: number }): Promise<{ message: string }> => {
        const row = await db.queryRow<Blog>`
        SELECT * FROM blogs WHERE id = ${id}`;
        if (!row) {
          throw APIError.notFound("找不到该博客");
        }

        await db.exec`DELETE FROM blogs WHERE id = ${id}`;

        return { message: "删除成功了" };
      }
    );

    // 异步函数用于删除指定ID的博客文章

进入全屏模式,退出全屏模式

这里提供了删除端点的测试版。前往开发者仪表盘页面,找到deleteBlog端点,然后传入你想要删除的博客对应的id

图片: 图片

现在我们已经了解了我们博客API的所有CRUD operations,包括创建、读取、更新和删除这四个操作,你已经有了构建一个功能齐全的应用的基础。

从创建新博客到读取、更新甚至删除它,我们已经涵盖了基础知识。

希望这能帮助你掌握如何使用Encore创建和管理你的应用。总有更多可以探索的地方,但这无疑是一个很好的起点。

启动后端服务器:

Encore 里部署东西感觉有点像魔法,既好玩又简单。

只需使用3个命令,就可以部署包括数据库在内的整个后端。

它是这么工作的:

  1. 提交你的更改
git add .  <!-- 将所有文件添加到暂存区 (Add all files to the staging area) -->
  1. 提交你的修改 带上消息:
git commit -m "你的提交信息"
  1. 再来一次精彩
git push 再次

就这样!Encore会搞定剩下的部分,构建你的应用,配置数据库,并将所有东西部署到云上。

图片 说明:此文本为图片链接。

现在请从app.encore.dev打开你的应用程序。

图片 这是一张示例图片。

这里是你实时提供的托管URL!你现在可以使用这个URL来创建阅读更新删除博客文章。

猜猜看怎么样?它已经被部署到了一个测试环境中。

那有多酷!只需要一分钟,你的应用就能运行起来,就可以开始测试和使用了?

再次让部署变得轻松自如,咱们试试看!(encore)

前端与 API 的集成

我创建了几个博客来展示前端。使用部署后的实时链接,我通过这个实时链接向数据库添加了4篇博客文章

从搭建后端到填充真实数据,看到这一切迅速完成真让人激动。

这些博客现在可以在前端展示了。

Postman

在这里,我创建了一个简单的Next.js 前端应用(前端项目)来展示出我们的API如何与任何前端进行集成。

无论是构建简洁现代的用户界面,还是极简主义的界面,后端都已准备好处理这些情况。

结构良好的API的美妙之处在于,它能与任何你能想象到的前端设计无缝对接。

    "use client";
    import axios from "axios";
    import { useEffect, useState } from "react";

    export default function BlogList() {
      const [blogs, setBlogs] = useState<
        {
          id: string;
          title: string;
          content: string;
          author: string;
          created_at: string;
        }[]
      >([]);
      const [loading, setLoading] = useState(true);
      const [error, setError] = useState<string | null>(null);

      useEffect(() => {
        const fetchBlogs = async () => {
          try {
            const response = await axios.get(
              "https://staging-blogs-nqei.encr.app/blogs"
            );
            console.log(response);
            setBlogs(response?.data?.blogs);
          } catch (err) {
            console.log(err);
            setError("获取博客失败,请稍后再试。");
          } finally {
            setLoading(false);
          }
        };
        fetchBlogs();
      }, []);

      if (loading) {
        return (
          <div className="text-center mt-20 text-lg text-gray-500">
            正在加载博客...
          </div>
        );
      }

      if (error) {
        return (
          <div className="text-center mt-20 text-lg text-red-500">{error}</div>
        );
      }

      return (
        <div className="min-h-screen bg-gradient-to-b from-blue-50 to-blue-100 py-10 px-4 sm:px-10">
          <h1 className="text-4xl font-bold text-center text-gray-700 mb-10 drop-shadow-lg">
            Syket的博客
          </h1>
          <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
            {blogs.map((blog) => (
              <div
                key={blog.id}
                className="bg-white shadow-md rounded-2xl p-6 hover:shadow-xl transition-shadow duration-300"
              >
                <h2 className="text-xl font-semibold text-gray-800 mb-2 truncate">
                  {blog.title}
                </h2>
                <p className="text-gray-600 text-sm line-clamp-3">{blog.content}</p>
              </div>
            ))}
          </div>
        </div>
      );
    }

进入全屏, 退出全屏

而这正是我们的简洁的小博客页面!它没什么花里胡哨的,但确实挺好用的。你可以看到我们之前创建的博客就显示在这里,直接从我们的后端API拿来的。

这真是个好例子,展示了各项是如何紧密联系在一起的,前后端配合得恰到好处。

示例

结尾

从项目最初的设置到现在我们创建了第一个API,将其连接到PostgreSQL数据库,再到将其部署在Encore环境中,这对我们来说一直是一段非凡的旅程。

有那么多的事要做,也有那么多的好景可看。当所有的部分汇聚在一起时,Encore 让设计强大且类型安全的应用程序变得简单易行,这些应用程序可以迅速扩展规模。

如果你想更深入了解,可以看看Documentation。Encore有很多好玩的东西等你来试试。

如果你喜欢这个项目,请在 GitHub 上给 Encore 点个星。

谢谢大家看到最后!

点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消