快速入门: SolidJS

介绍

这个例子提供了建立一个基本用户管理应用程序的步骤。它包括。

  • MemFire Cloud Database:一个用于存储用户数据的Postgres数据库。
  • MemFire Cloud Auth:用户可以用魔法链接登录(没有密码,只有电子邮件)。
  • MemFire Cloud 存储:用户可以上传照片。
  • 行级安全:数据受到保护,个人只能访问自己的数据。
  • 即时APIs。当你创建你的数据库表时,API将自动生成。

在本指南结束时,你将拥有一个允许用户登录和更新一些基本档案细节的应用程序。

Supabase用户管理实例

GitHub#

如果你在阅读指南时遇到困难,请参考此版本.

项目设置

在我们开始构建之前,我们要设置我们的数据库和API。这就像在Supabase中启动一个新项目一样简单 然后在数据库中创建一个 "模式"。

创建一个项目

  1. 进入MemFire Cloud
  2. 点击 "新项目"。
  3. 输入你的项目细节。
  4. 等待新数据库的启动。

设置数据库模式

现在我们要设置数据库模式。我们可以使用SQL编辑器中的 "用户管理"的模板快速启动。 或者你可以直接复制/粘贴下面的SQL,然后自己运行它。

  1. 进入仪表版中的SQL编辑器页面。
  2. 点击 用户管理的模板。
  3. 点击 运行

获取API密钥#

现在你已经创建了一些数据库表,你已经准备好使用自动生成的API插入数据。 我们只需要从API设置中获得URL和anon密钥。

  1. 进入仪表板中的设置页面。
  2. 单击侧边栏中的API
  3. 在这个页面上找到你的APIURLanonservice_role键。

构建应用程序

让我们开始从头开始构建SolidJS应用程序。

初始化一个SolidJS应用程序#

我们可以使用Degit来初始化一个名为supabase-solid的应用程序。

npx degit solidjs/templates/ts supabase-solid
cd supabase-solid

然后让我们安装唯一的额外依赖:supabase-js

npm install @supabase/supabase-js

最后,我们要把环境变量保存在.env中。 我们所需要的是API URL和你[早些时候]复制的anon密钥(#get-theapi-keys)。

.env
VITE_SUPABASE_URL=YOUR_SUPABASE_URL
VITE_SUPABASE_ANON_KEY=YOUR_SUPABASE_ANON_KEY

现在我们已经有了API凭证,让我们创建一个辅助文件来初始化Supabase客户端。这些变量将被暴露在 在浏览器上,这完全没有问题,因为我们的数据库已经启用了行级安全

src/supabaseClient.jsx
1import { createClient } from '@supabase/supabase-js'
2
3const supabaseUrl = import.meta.env.VITE_SUPABASE_URL
4const supabaseAnonKey = import.meta.env.VITE_SUPABASE_ANON_KEY
5
6export const supabase = createClient(supabaseUrl, supabaseAnonKey)

还有一个可选的步骤是更新CSS文件src/index.css以使应用程序看起来漂亮。 你可以找到这个文件的全部内容这里

设置一个登录组件

让我们设置一个 SolidJS 组件来管理登录和注册。我们将使用Magic Links,因此用户可以用他们的电子邮件登录,而无需使用密码。

src/Auth.tsx
1import { createSignal } from 'solid-js'
2import { supabase } from './supabaseClient'
3
4export default function Auth() {
5  const [loading, setLoading] = createSignal(false)
6  const [email, setEmail] = createSignal('')
7
8  const handleLogin = async (e: SubmitEvent) => {
9    e.preventDefault()
10
11    try {
12      setLoading(true)
13      const { error } = await supabase.auth.signInWithOtp({ email: email() })
14      if (error) throw error
15      alert('Check your email for the login link!')
16    } catch (error) {
17      if (error instanceof Error) {
18        alert(error.message)
19      }
20    } finally {
21      setLoading(false)
22    }
23  }
24
25  return (
26    <div class="row flex-center flex">
27      <div class="col-6 form-widget" aria-live="polite">
28        <h1 class="header">Supabase + SolidJS</h1>
29        <p class="description">Sign in via magic link with your email below</p>
30        <form class="form-widget" onSubmit={handleLogin}>
31          <div>
32            <label for="email">Email</label>
33            <input
34              id="email"
35              class="inputField"
36              type="email"
37              placeholder="Your email"
38              value={email()}
39              onChange={(e) => setEmail(e.currentTarget.value)}
40            />
41          </div>
42          <div>
43            <button type="submit" class="button block" aria-live="polite">
44              {loading() ? <span>Loading</span> : <span>Send magic link</span>}
45            </button>
46          </div>
47        </form>
48      </div>
49    </div>
50  )
51}

账号页面

在用户登录后,我们可以让他们编辑他们的个人资料细节和管理他们的账户。

让我们为它创建一个新的组件,叫做Account.tsx

src/Account.tsx
1import { AuthSession } from '@supabase/supabase-js'
2import { Component, createEffect, createSignal } from 'solid-js'
3import { supabase } from './supabaseClient'
4
5interface Props {
6  session: AuthSession;
7}
8
9const Account: Component<Props> = ({ session }) => {
10  const [loading, setLoading] = createSignal(true)
11  const [username, setUsername] = (createSignal < string) | (null > null)
12  const [website, setWebsite] = (createSignal < string) | (null > null)
13  const [avatarUrl, setAvatarUrl] = (createSignal < string) | (null > null)
14
15  createEffect(() => {
16    getProfile()
17  })
18
19  const getProfile = async () => {
20    try {
21      setLoading(true)
22      const { user } = session
23
24      let { data, error, status } = await supabase
25        .from('profiles')
26        .select(`username, website, avatar_url`)
27        .eq('id', user.id)
28        .single()
29
30      if (error && status !== 406) {
31        throw error
32      }
33
34      if (data) {
35        setUsername(data.username)
36        setWebsite(data.website)
37        setAvatarUrl(data.avatar_url)
38      }
39    } catch (error) {
40      if (error instanceof Error) {
41        alert(error.message)
42      }
43    } finally {
44      setLoading(false)
45    }
46  }
47
48  const updateProfile = async (e: Event) => {
49    e.preventDefault()
50
51    try {
52      setLoading(true)
53      const { user } = session
54
55      const updates = {
56        id: user.id,
57        username: username(),
58        website: website(),
59        avatar_url: avatarUrl(),
60        updated_at: new Date().toISOString(),
61      }
62
63      let { error } = await supabase.from('profiles').upsert(updates)
64
65      if (error) {
66        throw error
67      }
68    } catch (error) {
69      if (error instanceof Error) {
70        alert(error.message)
71      }
72    } finally {
73      setLoading(false)
74    }
75  }
76
77  return (
78    <div aria-live="polite">
79      <form onSubmit={updateProfile} class="form-widget">
80        <div>Email: {session.user.email}</div>
81        <div>
82          <label for="username">Name</label>
83          <input
84            id="username"
85            type="text"
86            value={username() || ''}
87            onChange={(e) => setUsername(e.currentTarget.value)}
88          />
89        </div>
90        <div>
91          <label for="website">Website</label>
92          <input
93            id="website"
94            type="text"
95            value={website() || ''}
96            onChange={(e) => setWebsite(e.currentTarget.value)}
97          />
98        </div>
99        <div>
100          <button type="submit" class="button primary block" disabled={loading()}>
101            {loading() ? 'Saving ...' : 'Update profile'}
102          </button>
103        </div>
104        <button type="button" class="button block" onClick={() => supabase.auth.signOut()}>
105          Sign Out
106        </button>
107      </form>
108    </div>
109  )
110}
111
112export default Account

启动

现在我们有了所有的组件,让我们更新App.tsx

src/App.tsx
1import { Component, createEffect, createSignal } from 'solid-js'
2import { supabase } from './supabaseClient'
3import { AuthSession } from '@supabase/supabase-js'
4import Account from './Account'
5import Auth from './Auth'
6
7const App: Component = () => {
8  const [session, setSession] = createSignal<AuthSession | null>(null)
9
10  createEffect(() => {
11    supabase.auth.getSession().then(({ data: { session } }) => {
12      setSession(session)
13    })
14
15    supabase.auth.onAuthStateChange((_event, session) => {
16      setSession(session)
17    })
18  })
19
20  return (
21    <div class="container" style={{ padding: '50px 0 100px 0' }}>
22      {!session() ? <Auth /> : <Account session={session()!} />}
23    </div>
24  )
25}
26
27export default App

一旦完成,在终端窗口运行这个程序。

npm start

然后打开浏览器到localhost:3000,你应该看到完成的应用程序。

Supabase SolidJS

个人照片

每个Supabase项目都配置了存储,用于管理照片和视频等大文件。

创建一个上传小组件

让我们为用户创建一个头像,以便他们可以上传个人资料照片。我们可以从创建一个新的组件开始。

src/Avatar.tsx
1import { Component, createEffect, createSignal, JSX } from 'solid-js'
2import { supabase } from './supabaseClient'
3
4interface Props {
5  size: number
6  url: string | null
7  onUpload: (event: Event, filePath: string) => void
8}
9
10const Avatar: Component<Props> = (props) => {
11  const [avatarUrl, setAvatarUrl] = createSignal<string | null>(null)
12  const [uploading, setUploading] = createSignal(false)
13
14  createEffect(() => {
15    if (props.url) downloadImage(props.url)
16  })
17
18  const downloadImage = async (path: string) => {
19    try {
20      const { data, error } = await supabase.storage.from('avatars').download(path)
21      if (error) {
22        throw error
23      }
24      const url = URL.createObjectURL(data)
25      setAvatarUrl(url)
26    } catch (error) {
27      if (error instanceof Error) {
28        console.log('Error downloading image: ', error.message)
29      }
30    }
31  }
32
33  const uploadAvatar: JSX.EventHandler<HTMLInputElement, Event> = async (event) => {
34    try {
35      setUploading(true)
36
37      const target = event.currentTarget
38      if (!target?.files || target.files.length === 0) {
39        throw new Error('You must select an image to upload.')
40      }
41
42      const file = target.files[0]
43      const fileExt = file.name.split('.').pop()
44      const fileName = `${Math.random()}.${fileExt}`
45      const filePath = `${fileName}`
46
47      let { error: uploadError } = await supabase.storage.from('avatars').upload(filePath, file)
48
49      if (uploadError) {
50        throw uploadError
51      }
52
53      props.onUpload(event, filePath)
54    } catch (error) {
55      if (error instanceof Error) {
56        alert(error.message)
57      }
58    } finally {
59      setUploading(false)
60    }
61  }
62
63  return (
64    <div style={{ width: props.size }} aria-live="polite">
65      {avatarUrl() ? (
66        <img
67          src={avatarUrl()!}
68          alt={avatarUrl() ? 'Avatar' : 'No image'}
69          class="avatar image"
70          style={{ height: `${props.size}px`, width: `${props.size}px` }}
71        />
72      ) : (
73        <div
74          class="avatar no-image"
75          style={{ height: `${props.size}px`, width: `${props.size}px` }}
76        />
77      )}
78      <div style={{ width: `${props.size}px` }}>
79        <label class="button primary block" for="single">
80          {uploading() ? 'Uploading ...' : 'Upload avatar'}
81        </label>
82        <span style="display:none">
83          <input
84            type="file"
85            id="single"
86            accept="image/*"
87            onChange={uploadAvatar}
88            disabled={uploading()}
89          />
90        </span>
91      </div>
92    </div>
93  )
94}
95
96export default Avatar

添加新的小组件

然后我们就可以把这个小部件添加到账号页面:

src/Account.tsx
1// Import the new component
2import Avatar from './Avatar'
3
4// ...
5
6return (
7  <form onSubmit={updateProfile} class="form-widget">
8    {/* Add to the body */}
9    <Avatar
10      url={avatarUrl()}
11      size={150}
12      onUpload={(e: Event, url: string) => {
13        setAvatarUrl(url)
14        updateProfile(e)
15      }}
16    />
17    {/* ... */}
18  </div>
19)

下一步

在这个阶段,你已经有了一个功能完备的应用程序!