用 RedwoodJS 构建一个用户管理应用
本教程演示了如何构建一个基本的用户管理应用。该应用可以进行用户认证和识别,将用户的个人资料信息存储在数据库中,并允许用户登录、更新他们的个人资料信息以及上传头像。该应用使用了:
🌐 This tutorial demonstrates how to build a basic user management app. The app authenticates and identifies the user, stores their profile information in the database, and allows the user to log in, update their profile details, and upload a profile photo. The app uses:
- Supabase 数据库 - 一个用于存储用户数据的 Postgres 数据库,并且有 行级安全,所以数据是受保护的,用户只能访问自己的信息。
- Supabase Auth - 允许用户注册和登录。
- Supabase 存储 - 允许用户上传个人头像。

如果你在跟着这个指南操作时遇到困难,你可以在GitHub上找到完整示例。
🌐 If you get stuck while working through this guide, you can find the full example on GitHub.
关于 RedwoodJS #
🌐 About RedwoodJS
Redwood 应用分为两部分:前端和后端。这表现为单个 monorepo 中的两个 Node 项目。
🌐 A Redwood application is split into two parts: a frontend and a backend. This is represented as two node projects within a single monorepo.
前端项目叫 web,后端项目叫 api。为了便于说明,我们在文章中会称它们为 “端””,也就是 web side 和 api side。它们是分开的项目,因为 web side 上的代码最终会在用户的浏览器中运行,而 api side 上的代码会在某个服务器上运行。
🌐 The frontend project is called web and the backend project is called api. For clarity, we will refer to these in prose as "sides," that is, the web side and the api side.
They are separate projects because code on the web side will end up running in the user's browser while code on the api side will run on a server somewhere.
重要提示:当本指南提到“API”时,是指 Supabase API;当提到 api side 时,是指 RedwoodJS 的 api side。
🌐 Important: When this guide refers to "API," that means the Supabase API and when it refers to api side, that means the RedwoodJS api side.
api side 是一个 GraphQL API 的实现。业务逻辑被组织成“服务”,这些服务有自己的内部 API,既可以被外部的 GraphQL 请求调用,也可以被其他内部服务调用。
🌐 The api side is an implementation of a GraphQL API. The business logic is organized into "services" that represent their own internal API and can be called both from external GraphQL requests and other internal services.
web side 是用 React 构建的。Redwood 的路由让你可以将 URL 路径映射到 React “页面”组件(并在每条路由上自动进行代码拆分)。
页面可以包含一个“布局”组件来封装内容。它们还包含“Cells”和普通的 React 组件。
Cells 让你可以以声明式的方式管理获取和显示数据的组件的生命周期。
🌐 The web side is built with React. Redwood's router lets you map URL paths to React "Page" components (and automatically code-split your app on each route).
Pages may contain a "Layout" component to wrap content. They also contain "Cells" and regular React components.
Cells allow you to declaratively manage the lifecycle of a component that fetches and displays data.
为了与其他框架教程保持一致,我们这次会以稍微不同的方式来构建这个应用。
我们不会像在 Redwood 应用中通常那样使用 Prisma 来连接 Supabase Postgres 数据库或使用 Prisma 迁移 。
相反,我们会依赖 Supabase 客户端在 web 一侧完成部分工作,并在 api 一侧再次使用客户端进行数据获取。
🌐 For the sake of consistency with the other framework tutorials, we'll build this app a little differently than normal.
We won't use Prisma to connect to the Supabase Postgres database or Prisma migrations as one typically might in a Redwood app.
Instead, we'll rely on the Supabase client to do some of the work on the web side and use the client again on the api side to do data fetching as well.
这意味着你会想要避免运行任何 yarn rw prisma migrate 命令,并且在部署时仔细检查你的构建命令,以确保 Prisma 不会重置你的数据库。Prisma 目前不支持跨 schema 外键,所以由于你的 Supabase public schema 引用了 auth.users,在 introspect schema 时会失败。
🌐 That means you will want to refrain from running any yarn rw prisma migrate commands and also double check your build commands on deployment to ensure Prisma won't reset your database. Prisma currently doesn't support cross-schema foreign keys, so introspecting the schema fails due
to how your Supabase public schema references the auth.users.
项目设置 #
🌐 Project setup
在你开始构建之前,你需要先设置数据库和 API。你可以通过在 Supabase 中启动一个新项目,然后在数据库中创建一个“架构”来完成这一步。
🌐 Before you start building you need to set up the Database and API. You can do this by starting a new Project in Supabase and then creating a "schema" inside the database.
创建一个项目 #
🌐 Create a project
- 在 Supabase 仪表板中创建一个新项目。
- 输入你的项目详情。
- 等新数据库上线。
设置数据库模式 #
🌐 Set up the database schema
现在设置数据库模式。你可以在 SQL 编辑器中使用“用户管理入门”快速开始,也可以复制粘贴下面的 SQL 并运行。
🌐 Now set up the database schema. You can use the "User Management Starter" quickstart in the SQL Editor, or you can copy/paste the SQL from below and run it.
- 在仪表板中转到SQL 编辑器页面。
- 点击 社区 > 快速入门 标签下的 用户管理入门。
- 点击运行。
你可以通过运行 db pull 命令将数据库架构拉到本地项目。查看本地开发文档获取详细说明。
🌐 You can pull the database schema down to your local project by running the db pull command. Read the local development docs for detailed instructions.
1supabase link --project-ref <project-id>2# You can get <project-id> from your project's dashboard URL: https://supabase.com/dashboard/project/<project-id>3supabase db pull获取 API 详情 #
🌐 Get API details
要与数据库表中的数据进行交互,你可以使用封装了自动生成的数据 API 端点的客户端库,并使用来自项目 Connect 对话框的项目 URL 和密钥进行认证。
🌐 To interact with data in database tables, you use the client libraries that wrap the auto-generated Data API endpoints, authenticating using the Project URL and key from the project Connect dialog.
阅读 API 密钥文档 以全面了解所有密钥类型、用途以及在哪里可以找到它们。
正在构建应用 #
🌐 Building the app
从零开始搭建 RedwoodJS 应用。
🌐 Build the RedwoodJS app from scratch.
RedwoodJS 需要 Node.js >= 14.x <= 16.x 和 Yarn >= 1.15。
🌐 RedwoodJS requires Node.js >= 14.x <= 16.x and Yarn >= 1.15.
确保你已经安装了 yarn,因为 RedwoodJS 依赖它来在工作区管理其包,用于其 web 和 api “端”。
🌐 Make sure you have installed yarn since RedwoodJS relies on it to manage its packages in workspaces for its web and api "sides."
初始化一个 RedwoodJS 应用 #
🌐 Initialize a RedwoodJS app
我们可以使用 Create Redwood App 命令来初始化一个名为 supabase-redwoodjs 的应用:
🌐 We can use Create Redwood App command to initialize
an app called supabase-redwoodjs:
1yarn create redwood-app supabase-redwoodjs2cd supabase-redwoodjs在应用安装的时候,你应该会看到:
🌐 While the app is installing, you should see:
1✔ Creating Redwood app2 ✔ Checking node and yarn compatibility3 ✔ Creating directory 'supabase-redwoodjs'4✔ Installing packages5 ✔ Running 'yarn install'... (This could take a while)6✔ Convert TypeScript files to JavaScript7✔ Generating types89Thanks for trying out Redwood!然后通过运行 setup auth 命令来安装唯一的额外依赖 supabase-js:
🌐 Then install the only additional dependency supabase-js by running the setup auth command:
1yarn redwood setup auth supabase当被提示时:
🌐 When prompted:
是否覆盖现有的 /api/src/lib/auth.[jt]s?
说 是,它会在你的应用中设置 Supabase 客户端,并且提供用于 Supabase 认证的钩子。
🌐 Say, yes and it will setup the Supabase client in your app and also provide hooks used with Supabase authentication.
1✔ Generating auth lib...2 ✔ Successfully wrote file `./api/src/lib/auth.js`3 ✔ Adding auth config to web...4 ✔ Adding auth config to GraphQL API...5 ✔ Adding required web packages...6 ✔ Installing packages...7 ✔ One more thing...89 You will need to add your Supabase URL (SUPABASE_URL), public API KEY,10 and JWT SECRET (SUPABASE_KEY, and SUPABASE_JWT_SECRET) to your .env file.接下来,我们想要把环境变量保存到一个 .env。我们还需要 API URL 以及你之前 复制 的密钥和 jwt_secret。
🌐 Next, we want to save the environment variables in a .env.
We need the API URL as well as the key and jwt_secret that you copied earlier.
1SUPABASE_URL=YOUR_SUPABASE_URL2SUPABASE_KEY=YOUR_SUPABASE_PUBLISHABLE_KEY3SUPABASE_JWT_SECRET=YOUR_SUPABASE_JWT_SECRET最后,你还需要将 仅 web side 环境变量保存到 redwood.toml。
🌐 And finally, you will also need to save only the web side environment variables to the redwood.toml.
1[web]2 title = "Supabase Redwood Tutorial"3 port = 89104 apiProxyPath = "/.redwood/functions"5 includeEnvironmentVariables = ["SUPABASE_URL", "SUPABASE_KEY"]6[api]7 port = 89118[browser]9 open = true这些变量会在浏览器上暴露,这完全没问题。它们允许你的网页应用使用你的可公开密钥初始化 Supabase 客户端,因为我们的数据库启用了 行级安全。
🌐 These variables will be exposed on the browser, and that's completely fine. They allow your web app to initialize the Supabase client with your publishable key since we have Row Level Security enabled on our Database.
你会看到这些被用来在 web/src/App.js 中配置你的 Supabase 客户端:
🌐 You'll see these being used to configure your Supabase client in web/src/App.js:
1// ... Redwood imports2import { AuthProvider } from '@redwoodjs/auth'3import { createClient } from '@supabase/supabase-js'45// ...67const supabase = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_KEY)89const App = () => (10 <FatalErrorBoundary page={FatalErrorPage}>11 <RedwoodProvider titleTemplate="%PageTitle | %AppTitle">12 <AuthProvider client={supabase} type="supabase">13 <RedwoodApolloProvider>14 <Routes />15 </RedwoodApolloProvider>16 </AuthProvider>17 </RedwoodProvider>18 </FatalErrorBoundary>19)2021export default App应用风格(可选) #
🌐 App styling (optional)
一个可选步骤是更新 CSS 文件 web/src/index.css,让应用看起来更好。你可以在示例仓库中找到这个文件的完整内容。
🌐 An optional step is to update the CSS file web/src/index.css to make the app look better.
You can find the full contents of this file in the example repository.
启动 RedwoodJS 和你的第一个页面 #
🌐 Start RedwoodJS and your first page
通过启动应用来测试你的设置:
🌐 Test your setup by starting the app:
1yarn rw devrw 是 redwood 的别名,就像用 yarn rw 来运行 Redwood CLI 命令一样。
你应该会看到一个“欢迎来到 RedwoodJS”的页面,以及一条关于还没有任何页面的消息。
🌐 You should see a "Welcome to RedwoodJS" page and a message about not having any pages yet.
创建一个“主页”:
🌐 Create a "home" page:
1yarn rw generate page home /23✔ Generating page files...4 ✔ Successfully wrote file `./web/src/pages/HomePage/HomePage.stories.js`5 ✔ Successfully wrote file `./web/src/pages/HomePage/HomePage.test.js`6 ✔ Successfully wrote file `./web/src/pages/HomePage/HomePage.js`7✔ Updating routes file...8✔ Generating types .../ 在这里很重要,因为它创建了一个根级路由。
🌐 The / is important here as it creates a root level route.
如果你愿意,可以停止 dev 服务器;要查看你的更改,再运行一次 yarn rw dev。
🌐 You can stop the dev server if you want; to see your changes, run yarn rw dev again.
你应该在 web/src/Routes.js 中看到 Home 页面路由:
🌐 You should see the Home page route in web/src/Routes.js:
1import { Router, Route } from '@redwoodjs/router'23const Routes = () => {4 return (5 <Router>6 <Route path="/" page={HomePage} name="home" />7 <Route notfound page={NotFoundPage} />8 </Router>9 )10}1112export default Routes设置一个登录组件 #
🌐 Set up a login component
设置一个 Redwood 组件来管理登录和注册。我们将使用 Magic Links,这样用户可以用邮箱登录而不用密码。
🌐 Set up a Redwood component to manage logins and sign ups. We'll use Magic Links, so users can sign in with their email without using passwords.
1yarn rw g component auth23 ✔ Generating component files...4 ✔ Successfully wrote file `./web/src/components/Auth/Auth.test.js`5 ✔ Successfully wrote file `./web/src/components/Auth/Auth.stories.js`6 ✔ Successfully wrote file `./web/src/components/Auth/Auth.js`现在,更新 Auth.js 组件,使其包含:
🌐 Now, update the Auth.js component to contain:
1import { useAuth } from '@redwoodjs/auth'2import { useState } from 'react'34const Auth = () => {5 const { logIn } = useAuth()6 const [loading, setLoading] = useState(false)7 const [email, setEmail] = useState('')89 const handleLogin = async (email) => {10 try {11 setLoading(true)12 const { error } = await logIn({ email })13 if (error) throw error14 alert('Check your email for the login link!')15 } catch (error) {16 alert(error.error_description || error.message)17 } finally {18 setLoading(false)19 }20 }2122 return (23 <div className="row flex-center flex">24 <div className="col-6 form-widget">25 <h1 className="header">Supabase + RedwoodJS</h1>26 <p className="description">Sign in via magic link with your email below</p>27 <div>28 <input29 className="inputField"30 type="email"31 placeholder="Your email"32 value={email}33 onChange={(e) => setEmail(e.target.value)}34 />35 </div>36 <div>37 <button38 onClick={(e) => {39 e.preventDefault()40 handleLogin(email)41 }}42 className={'button block'}43 disabled={loading}44 >45 {loading ? <span>Loading</span> : <span>Send magic link</span>}46 </button>47 </div>48 </div>49 </div>50 )51}5253export default Auth设置一个账户组件 #
🌐 Set up an account component
用户登录后,我们可以允许他们编辑个人资料信息并管理他们的账户。
🌐 After a user is signed in we can allow them to edit their profile details and manage their account.
创建一个叫做 Account.js 的新组件。
🌐 Create a new component called Account.js.
1yarn rw g component account23 ✔ Generating component files...4 ✔ Successfully wrote file `./web/src/components/Account/Account.test.js`5 ✔ Successfully wrote file `./web/src/components/Account/Account.stories.js`6 ✔ Successfully wrote file `./web/src/components/Account/Account.js`然后更新文件以包含:
🌐 And then update the file to contain:
1import { useAuth } from '@redwoodjs/auth'2import { useEffect, useState } from 'react'34const Account = () => {5 const { client: supabase, currentUser, logOut } = useAuth()6 const [loading, setLoading] = useState(true)7 const [username, setUsername] = useState(null)8 const [website, setWebsite] = useState(null)9 const [avatar_url, setAvatarUrl] = useState(null)1011 useEffect(() => {12 getProfile()13 }, [supabase.auth.session])1415 async function getProfile() {16 try {17 setLoading(true)18 const user = supabase.auth.user()1920 const { data, error, status } = await supabase21 .from('profiles')22 .select(`username, website, avatar_url`)23 .eq('id', user.id)24 .single()2526 if (error && status !== 406) {27 throw error28 }2930 if (data) {31 setUsername(data.username)32 setWebsite(data.website)33 setAvatarUrl(data.avatar_url)34 }35 } catch (error) {36 alert(error.message)37 } finally {38 setLoading(false)39 }40 }4142 async function updateProfile({ username, website, avatar_url }) {43 try {44 setLoading(true)45 const user = supabase.auth.user()4647 const updates = {48 id: user.id,49 username,50 website,51 avatar_url,52 updated_at: new Date(),53 }5455 const { error } = await supabase.from('profiles').upsert(updates, {56 returning: 'minimal', // Don't return the value after inserting57 })5859 if (error) {60 throw error61 }6263 alert('Updated profile!')64 } catch (error) {65 alert(error.message)66 } finally {67 setLoading(false)68 }69 }7071 return (72 <div className="row flex-center flex">73 <div className="col-6 form-widget">74 <h1 className="header">Supabase + RedwoodJS</h1>75 <p className="description">Your profile</p>76 <div className="form-widget">77 <div>78 <label htmlFor="email">Email</label>79 <input id="email" type="text" value={currentUser.email} disabled />80 </div>81 <div>82 <label htmlFor="username">Name</label>83 <input84 id="username"85 type="text"86 value={username || ''}87 onChange={(e) => setUsername(e.target.value)}88 />89 </div>90 <div>91 <label htmlFor="website">Website</label>92 <input93 id="website"94 type="url"95 value={website || ''}96 onChange={(e) => setWebsite(e.target.value)}97 />98 </div>99100 <div>101 <button102 className="button primary block"103 onClick={() => updateProfile({ username, website, avatar_url })}104 disabled={loading}105 >106 {loading ? 'Loading ...' : 'Update'}107 </button>108 </div>109110 <div>111 <button className="button block" onClick={() => logOut()}>112 Sign Out113 </button>114 </div>115 </div>116 </div>117 </div>118 )119}120121export default Account你会多次看到 useAuth() 的使用。Redwood 的 useAuth 钩子提供了方便的方式来访问 logIn、logOut、currentUser,以及访问 supabase 认证客户端。我们会用它来获取一个 Supabase 客户端实例,以便与你的 API 交互。
🌐 You'll see the use of useAuth() several times. Redwood's useAuth hook provides convenient ways to access
logIn, logOut, currentUser, and access the supabase authenticate client. We'll use it to get an instance
of the Supabase client to interact with your API.
更新首页 #
🌐 Update home page
在所有组件就位后,更新你的 HomePage 页面来使用它们:
🌐 With all the components in place, update your HomePage page to use them:
1import { useAuth } from '@redwoodjs/auth'2import { MetaTags } from '@redwoodjs/web'3import Account from 'src/components/Account'4import Auth from 'src/components/Auth'56const HomePage = () => {7 const { isAuthenticated } = useAuth()89 return (10 <>11 <MetaTags title="欢迎" />12 {!isAuthenticated ? <Auth /> : <Account />}13 </>14 )15}1617export default HomePage我们在这里做的是,如果你没有登录,就显示登录表单;如果你已经登录,就显示你的账户资料。
🌐 What we're doing here is showing the sign in form if you aren't logged in and your account profile if you are.
头像照片 #
🌐 Profile photos
接下来,添加一个让用户上传个人资料照片的方式。Supabase 会为每个项目配置 Storage,用于管理像照片和视频这样的大文件。
🌐 Next, add a way for users to upload a profile photo. Supabase configures every project with Storage for managing large files like photos and videos.
创建一个上传小工具 #
🌐 Create an upload widget
创建一个头像,这样用户就可以上传个人照片。首先创建一个新组件:
🌐 Create an avatar so the user can upload a profile photo. Start by creating a new component:
1yarn rw g component avatar2 ✔ Generating component files...3 ✔ Successfully wrote file `./web/src/components/Avatar/Avatar.test.js`4 ✔ Successfully wrote file `./web/src/components/Avatar/Avatar.stories.js`5 ✔ Successfully wrote file `./web/src/components/Avatar/Avatar.js`现在,更新你的 Avatar 组件,使其包含以下小部件:
🌐 Now, update your Avatar component to contain the following widget:
1import { useAuth } from '@redwoodjs/auth'2import { useEffect, useState } from 'react'34const Avatar = ({ url, size, onUpload }) => {5 const { client: supabase } = useAuth()67 const [avatarUrl, setAvatarUrl] = useState(null)8 const [uploading, setUploading] = useState(false)910 useEffect(() => {11 if (url) downloadImage(url)12 }, [url])1314 async function downloadImage(path) {15 try {16 const { data, error } = await supabase.storage.from('avatars').download(path)17 if (error) {18 throw error19 }20 const url = URL.createObjectURL(data)21 setAvatarUrl(url)22 } catch (error) {23 console.log('Error downloading image: ', error.message)24 }25 }2627 async function uploadAvatar(event) {28 try {29 setUploading(true)3031 if (!event.target.files || event.target.files.length === 0) {32 throw new Error('You must select an image to upload.')33 }3435 const file = event.target.files[0]36 const fileExt = file.name.split('.').pop()37 const fileName = `${Math.random()}.${fileExt}`38 const filePath = `${fileName}`3940 const { error: uploadError } = await supabase.storage.from('avatars').upload(filePath, file)4142 if (uploadError) {43 throw uploadError44 }4546 onUpload(filePath)47 } catch (error) {48 alert(error.message)49 } finally {50 setUploading(false)51 }52 }5354 return (55 <div>56 {avatarUrl ? (57 <img58 src={avatarUrl}59 alt="Avatar"60 className="avatar image"61 style={{ height: size, width: size }}62 />63 ) : (64 <div className="avatar no-image" style={{ height: size, width: size }} />65 )}66 <div style={{ width: size }}>67 <label className="button primary block" htmlFor="single">68 {uploading ? 'Uploading ...' : 'Upload'}69 </label>70 <input71 style={{72 visibility: 'hidden',73 position: 'absolute',74 }}75 type="file"76 id="single"77 accept="image/*"78 onChange={uploadAvatar}79 disabled={uploading}80 />81 </div>82 </div>83 )84}8586export default Avatar触发! #
🌐 Launch!
完成后,在终端窗口中运行此命令以启动 dev 服务器:
🌐 Once that's done, run this in a terminal window to launch the dev server:
1yarn rw dev然后打开浏览器访问 localhost:8910,你应该能看到完成的应用。
🌐 And then open the browser to localhost:8910 and you should see the completed app.

在这个阶段,你已经有了一个完全可用的应用!
🌐 At this stage you have a fully functional application!
另请参阅 #
🌐 See also
- 了解更多关于 RedwoodJS 的信息
- 访问 RedwoodJS 讨论社区