如何在RedwoodJS中建立一个全栈的应用程序(附代码)

426 阅读9分钟

在本指南中,我们将介绍如何用RedwoodJS构建一个全栈应用程序。

你可能已经看到了很多围绕用x框架(或)技术构建全栈应用的教程和指南,但是,RedwoodJS在某些方面是不同的,而且是有益的,包括:

  1. RedwoodJS包括Typescript、GraphQL、Prisma和一个测试框架
  2. 初创公司可以构建和制作产品原型,因为它提供了认证、授权和CRUD操作等模块。我们需要做的就是为我们的需求设计业务逻辑。
  3. CLI是RedwoodJS最好的功能之一;它使开发过程更快、更容易。

在这里,我们要建立一个论坛来了解RedwoodJS应用程序是如何建立的。它包括所有的功能,帮助你了解所有框架的功能。

我们要建立的功能是:

  • 登录和注册
  • 创建、阅读和更新帖子
  • 评论系统
  • 基于用户的帖子访问

除了RedwoodJS,我们还将使用Typescript进行类型检查,并使用TailwindCSS进行样式设计。

RedwoodJS的安装和设置

RedwoodJS使用yarn作为一个包管理器。一旦你安装了它,你可以使用以下命令创建一个新项目:

yarn create redwood-app --ts ./redwoodblog

它将所有的模块搭成脚手架,以建立一个全栈的应用程序。在这里,你可以看到一个RedwoodJS应用程序的完整结构:

Redwood JS app folder structure

有三个主要的目录。它们是:apiscripts ,和web 。让我们详细讨论一下它们。

  • .redwood:包含应用程序的构建。
  • api:为应用程序的后端服务。它主要包含db ,它为应用程序的数据库模式服务。所有的后端功能都将在src 目录中。
    • src:包含你所有的后端代码。它包含五个目录,它们是:。
      • directives:包含GraphQL模式指令,以控制对GraphQL查询的访问。
      • functions:RedwoodJS将GraphQL API作为无服务器函数运行。它自动生成graphql.ts ;你可以在它上面添加额外的无服务器函数。
      • graphql:包含用模式定义语言(SDL)编写的GraphQL模式。
      • lib:包含整个后端API的所有可重复使用的功能,例如,认证
      • services:包含与你的数据有关的业务逻辑。它运行与API相关的功能并返回结果。

设置TailwindCSS

安装TailwindCSS是很简单的;在根目录下运行以下命令:

yarn rw setup ui tailwindcss

Installing packages

为了确认TailwindCSS的安装,请进入web/src/index.css ,并在该文件中看到Tailwind类。

连接数据库

为了连接Postgres数据库,我们将使用Docker进行本地开发。

(注意:要安装docker,请看docker官方网站的文档)

在根目录下创建docker-utils/postgres-database.sh ,并添加以下脚本:

#!/bin/bash

set -e
set -u

function create_user_and_database() {
        local database=$1
        echo "  Creating user and database '$database'"
        psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" <<-EOSQL
            CREATE USER $database;
            CREATE DATABASE $database;
            GRANT ALL PRIVILEGES ON DATABASE $database TO $database;
EOSQL
}

if [ -n "$POSTGRES_MULTIPLE_DATABASES" ]; then
        echo "Multiple database creation requested: $POSTGRES_MULTIPLE_DATABASES"
        for db in $(echo $POSTGRES_MULTIPLE_DATABASES | tr ',' ' '); do
                create_user_and_database $db
        done
        echo "Multiple databases created"
fi

这个脚本实现了一个在Postgres中创建用户和数据库的功能。一旦你创建了这个脚本,你就可以使用docker-compose up 来运行Postgres数据库。

创建docker-compose.yml ,并添加下面的代码:

version: "3.6"
services:
  postgres:
    image: postgres
    restart: unless-stopped

如果你想根据环境创建不同版本的docker-compose up ,你也可以这样做。要做到这一点,请创建docker-compose.override.yml ,并添加以下代码:

version: "3"
services:
  postgres:
    image: postgres
    environment:
      - POSTGRES_USER=api
      - POSTGRES_PASSWORD=development_pass
      - POSTGRES_MULTIPLE_DATABASES="redwoodforum-api","redwoodforum-api-testing"
    volumes:
      - ./docker-utils:/docker-entrypoint-initdb.d
      - redwoodforum_api_data:/data/postgres
    ports:
      - 5440:5432
volumes:
  redwoodforum_api_data: {}

一旦你添加了这个脚本,你就可以用这个命令运行数据库:

docker-compose up

Running database

要将红木应用程序连接到Postgres,将Prisma配置改为PostgreSQL提供者,并在环境变量中添加数据库URL。

转到api/db/schema.prisma ,并将db提供者改为postgresql 。在你的.env中添加DATABASE_URL

DATABASE_URL=postgres://api:development_pass@localhost:5440/redwoodforum-api

设计数据库

正如你在演示中看到的,我们想建立一个论坛。然而,在我们实现这些功能之前,以下是我们希望用户能够在我们的应用程序中做的关键事情:

  • 用户可以登录/注册到应用程序中
  • 一旦用户登录,他们可以在论坛中创建一个帖子
  • 用户可以对任何帖子发表评论,而主人可以删除任何评论
  • 用户可以查看自己的帖子,也可以到主页查看所有的帖子。

让我们为该应用程序设计一个ER图:

ER diagram for application depicts user comment and post

这里我们有user,post, 和comment 模式。

user 和 有一对多的关系, 和 有一对多的关系,而 和 有一对一的关系。post post comment comment user

现在我们有了应用程序的ER图。让我们为数据库创建模式。为此,请访问api/db/schema.prisma

(注意:RedwoodJS使用Prisma做数据库。如果你是Prisma世界的新手,请查看他们的文档以获得更多信息)

现在,在一个Prisma文件中创建模式:

model User {
  id           Int            @id @default(autoincrement())
  email        String         @unique
  name         String?
  hashedPassword     String
  salt String
  resetToken String?
  resetTokenExpiresAt DateTime?
  posts   Post[]
  comments Comment[]
}

model Post {
  id           Int            @id @default(autoincrement())
  title        String
  body         String
  comments     Comment[]
  author     User    @relation(fields: [authorId], references: [id])
  authorId   Int
  createdAt    DateTime  @default(now())
  updatedAt    DateTime  @default(now())
}

model Comment {
  id     Int    @id @default(autoincrement())
  body   String
  post   Post   @relation(fields: [postId], references: [id])
  postId Int
  author User   @relation(fields: [authorId], references: [id])
  authorId Int
  createdAt DateTime @default(now())
  updatedAt DateTime @default(now())
}

正如你所看到的,我们在User,Post, 和Comment 架构之间有一种关系。

在Prisma中定义一个关系是很简单的。你可以参考文档以了解更多细节。

一旦你在Prisma中定义了一个模式,你就必须运行迁移以在Postgres中把这些模式创建为一个表。

Prisma迁移

Prisma的一个特点是,你可以管理不同阶段的迁移。在这里,我们将只为开发运行迁移。为此,你可以使用这个命令:

yarn redwood prisma migrate dev

Database now in sync with schema

要检查迁移是否成功,你可以进入Prisma Studio,看到迁移后的所有表。你可以通过访问http://localhost:5555,看到所有的表和每个表内的列:

yarn redwood prisma studio

现在,我们有了API和前端的数据库和模式,让我们来为应用程序创建认证。

身份验证

RedwoodJS提供了开箱即用的认证。一条CLI命令就可以得到你所需要的一切,以使认证工作正常进行:

yarn rw setup auth dbAuth

它将创建一个auth.ts 无服务器函数,检查cookie是否存在于数据库中以及令牌是否过期。然后,它在此基础上向客户端返回响应。

它还创建了lib/auth.ts ,以处理各种功能,如从会话中获取当前用户,检查是否已认证,要求认证等。

到目前为止,我们有API和数据库的认证功能。让我们来创建登录、注册和忘记密码的页面。然后,你可以使用命令来构建登录、注册和忘记密码的页面:

yarn rw g dbAuth

它将创建所有用于认证的页面。你可以在web/src/pages 查看这些页面。

Looking at files under pages folder in overall structure

对于页面的样式,你可以使用源代码中的组件,并根据你的喜好进行定制。下面是实现中的完整的登录页面。

Login page

为了连接一个用于登录和注册功能的API,RedwoodJS提供了钩子,在引擎盖下完成所有的魔法:

import { useAuth } from '@redwoodjs/auth'

// provides login and signup functionality out of the box
const { isAuthenticated, signUp, logIn, logOut } = useAuth()

在表单onSubmit 函数中,我们可以使用该signuplogIn 来进行API请求并发送有效载荷:

const onSubmit = async (data) => {
    const response = await signUp({ ...data })

    if (response.message) {
      toast(response.message)
    } else if (response.error) {
      toast.error(response.error)
    } else {
      // user is signed in automatically
      toast.success('Welcome!')
    }
  }

一旦用户注册或登录,你可以使用currentUser ,在整个应用程序中访问用户信息:

const { currentUser } = useAuth()

现在,我们已经让用户登录到应用程序中。接下来,让我们建立发布和评论的功能。

一旦用户登录,他们就会进入主页,在那里我们需要显示论坛中的所有帖子。然后,用户可以创建一个新的帖子和更新一个帖子。

为了实现列表页,用主页组件创建一个路由,从API中获取数据,在客户端显示。

幸运的是,RedwoodJS提供了脚手架,为我们生成了所有的实现。假设你想为所有的页面搭建脚手架,包括GraphQL后端实现,你可以使用以下命令:

yarn redwood g scaffold post

它将生成页面、SDL和post模型的服务。你可以在RedwoodJS的文档中参考所有的命令。

由于我们要定制页面。让我们只对SDL和服务进行支架化。使用这个命令:

yarn redwood g sdl --typescript post

它将在graphql/posts.sdl.tsservices/posts 中创建后域文件 - 让我们在网上创建页面。

即使我们定制了页面和组件,我们也不需要从头开始创建一切。相反,我们可以使用脚手架,并根据我们的要求来修改它。

让我们用这个命令创建一个主页

yarn redwood g page home

它将创建一个主页,并将该页面添加到Routes.tsx 。 所以现在,你有了基本的主页组件。

现在,为了在主页上列出所有的帖子,你需要从api获取数据并在页面上显示。为了使这个过程更容易,RedwoodJS提供了单元格。单元是一种获取数据的声明性方法--它执行GraphQL查询并管理其生命周期。

要生成单元格,请使用这个命令:

yarn rw generate cell home

它将创建一个GraphQL查询和它的生命周期:

import type { FindPosts } from 'types/graphql'

import { Link } from '@redwoodjs/router'
import type {
  CellSuccessProps,
  CellFailureProps,
  CellLoadingProps,
} from '@redwoodjs/web'

export const QUERY = gql`
  query FindPosts {
    posts {
      id
      title
      body
      comments {
        id
      }
      createdAt
      updatedAt
    }
  }
`

export const Loading: React.FC<CellLoadingProps> = () => <div>Loading...</div>

export const Empty = () => <div>No posts found</div>

export const Failure = ({ error }: CellFailureProps) => (
  <div>Error loading posts: {error.message}</div>
)

export const Success = ({ posts }: CellSuccessProps<FindPosts>) => {
  return (
    <div>
      <ul>
        {posts.map((post) => (
          <li key={post.id}>
            {' '}
            <Link to={`/posts/${post.id}`}>
              <div className="p-2 my-2 rounded-lg shadow cursor-pointer">
                <h4 className="text-xl font-medium">{post.title}</h4>

                <p>{post.body}</p>
              </div>
            </Link>
          </li>
        ))}
      </ul>
    </div>
  )
}

受保护的路由

为了保护RedwoodJS应用程序中的路由,你可以使用Private ,从@redwoodjs/router ,并将所有东西包裹在路由内:

<Private unauthenticated="login">
        <Set wrap={NavbarLayout}>
          <Set wrap={ContainerLayout}>
            <Route path="/new" page={NewpostPage} name="newpost" />
            <Set wrap={SidebarLayout}>
              <Route path="/" page={HomePage} name="home" />
              // routes come here
            </Set>
          </Set>
        </Set>
</Private>

创建帖子

要创建一个新的帖子,使用以下命令搭建一个新的帖子页面的脚手架:

yarn redwood g page newpost /new

如果你想定制路由的URL,你可以在这里作为一个参数传递。RedwoodJS会根据提供的名称添加路由。RedwoodJS提供了开箱即用的表单和验证:

import {
  FieldError,
  Form,
  Label,
  TextField,
  TextAreaField,
  Submit,
  SubmitHandler,
} from '@redwoodjs/forms'

一旦用户提交表单,你可以调用GraphQL突变来创建一个帖子:

const CREATE_POST = gql`
  mutation CreatePostMutation($input: CreatePostInput!) {
    createPost(input: $input) {
      id
    }
  }
`
const onSubmit: SubmitHandler<FormValues> = async (data) => {
    try {
      await create({
        variables: {
          input: { ...data, authorId: currentUser.id },
        },
      })
      toast('Post created!')
      navigate(routes.home())
    } catch (e) {
      toast.error(e.message)
    }
  }

帖子的细节

创建一个帖子的详细信息页面和单元格,以获取数据来查看帖子的详细信息。你可以遵循我们之前所做的相同过程:

yarn redwood g page postdetails

这将在routes.tsx 中创建页面和路由。为了在路由中传递URL参数,你可以这样修改它:

<Route path="/posts/{id:Int}" page={PostDetails} name="postdetails" />

你可以把ID作为道具传入组件。然后,创建一个单元格来获取帖子的详细信息,并在组件中呈现:

yarn redwood g cell post

添加以下代码来获取特定帖子的数据和评论:

import type { FindPosts } from 'types/graphql'
import { format } from 'date-fns'
import { useAuth } from '@redwoodjs/auth'
import type {
  CellSuccessProps,
  CellFailureProps,
  CellLoadingProps,
} from '@redwoodjs/web'

export const QUERY = gql`
  query FindPostDetail($id: Int!) {
    post: post(id: $id) {
      id
      title
      body
      author {
        id
      }
      comments {
        id
        body
        author {
          id
          name
        }
        createdAt
      }
      createdAt
      updatedAt
    }
  }
`

export const Loading: React.FC<CellLoadingProps> = () => <div>Loading...</div>

export const Empty = () => <div>No posts found</div>

export const Failure = ({ error }: CellFailureProps) => (
  <div>Error loading posts: {error.message}</div>
)

export const Success = ({ post }: CellSuccessProps<FindPosts>) => {
  const { currentUser } = useAuth()

  return (
    <div>
      <div>
        <h2 className="text-2xl font-semibold">{post.title}</h2>
        <p className="mt-2">{post.body}</p>
      </div>

      <div className="mt-4 ">
        <hr />
        <h3 className="my-4 text-lg font-semibold text-gray-900">Comments</h3>
        {post.comments.map((comment) => (
          <div
            key={comment.id}
            className="flex justify-between sm:px-2 sm:py-2 border rounded-lg"
          >
            <div className="my-4 flex-1  leading-relaxed">
              <strong>{comment.author.name}</strong>{' '}
              <span className="text-xs text-gray-400">
                {format(new Date(comment.createdAt), 'MMM d, yyyy h:mm a')}
              </span>
              <p>{comment.body}</p>
            </div>
            {currentUser && currentUser.id === post.author.id && (
              <div className="m-auto">
                <button
                  type="button"
                  className="focus:outline-none text-white bg-red-700 hover:bg-red-800 focus:ring-4 focus:ring-red-300 font-medium rounded-lg text-sm px-5 py-2.5 mr-2 mb-2 dark:bg-red-600 dark:hover:bg-red-700 dark:focus:ring-red-900"
                >
                  Delete
                </button>
              </div>
            )}
          </div>
        ))}
      </div>
    </div>
  )
}

这里需要注意的是,检查当前登录的用户是否是帖子的作者的条件。在这种情况下,我们提供一个选项来删除评论。

基于用户的访问

为了在应用程序中提供基于用户的访问,你可以使用useAuth 钩子获得当前用户,并在上面添加条件。例如,为了显示用户创建的帖子列表,你可以使用当前的用户ID按作者来获取帖子:

const { currentUser } = useAuth()

MyPostCell.tsx

import { Link } from '@redwoodjs/router'
import type { FindPosts } from 'types/graphql'
import type {
  CellSuccessProps,
  CellFailureProps,
  CellLoadingProps,
} from '@redwoodjs/web'

export const QUERY = gql`
  query FindMyPosts($id: Int!) {
    user: user(id: $id) {
      id
      name
      posts {
        id
        title
        body
      }
    }
  }
`

export const Loading: React.FC<CellLoadingProps> = () => <div>Loading...</div>

export const Empty = () => <div>No posts found</div>

export const Failure = ({ error }: CellFailureProps) => (
  <div>Error loading posts: {error.message}</div>
)

export const Success = ({ user }: CellSuccessProps<FindPosts>) => {
  return (
    <div>
      <ul>
        {user.posts.map((post) => (
          <li key={post.id}>
            {' '}
            <Link to={`/posts/${post.id}`}>
              <div className="shadow rounded-lg p-2 my-2 cursor-pointer">
                <h4 className="text-xl font-medium">{post.title}</h4>

                <p>{post.body}</p>
              </div>
            </Link>
          </li>
        ))}
      </ul>
    </div>
  )
}

总结

RedwoodJS提供了开箱即用的一切。所有这些都是根据我们的要求来构建应用程序。一些重要的概念是单元格、页面、Prisma模式和迁移,以及了解系统如何工作。

一旦你了解了RedWoodJS,你就可以用很少的时间构建一个全栈的应用程序,正如我们在这篇文章中所看到的那样。你可以在这里找到本教程的源代码。