因此,您已经学习了SolidJS并构建了一堆演示,但是您还没有使用它创建真实的应用程序。
如果是您,并且您想学习如何使用SolidStart framework构建连接到GraphQl Server的服务器端渲染应用程序以加载和存储数据,则此教程适合您。
在本教程中,您将学习如何创建一个新项目,添加页面,实现身份验证,连接到GraphQl Server以及使用TailWindCSS的样式。
先决条件
在本教程中,我认为您知道SolidJ的基础知识,并且您至少通过SolidStart文档撇去了。
如果您以前从未使用过SolidJ,我鼓励您首先检查the official tutorial,然后继续此处。
我们要建造什么?
要专注于构建SolidStart应用程序的概念和工作流程,我们将构建一个简单的todo应用程序。
该应用程序将有三页:登录页面,注册页面和主页。首页将包含用户列表的列表。用户还应该能够添加新的戒酒,切换戒酒和删除todos。
在启动教程之前,最好在GitHub上检查代码(但这是可选的)。
GraphQl Server
我已经为此应用创建了后端项目。因此,您需要知道的只是GraphQl Server的URL。
要使用它,您必须先运行它。因此,您需要从GitHub克隆该项目并在本地运行。
好吧,这是一个漫长的介绍。如果您准备好了,让我们开始。
创建项目
首先,从终端运行:
mkdir todoapp-solid && cd todoapp-solid
pnpm create solid
您会看到一个入门模板列表。选择with-tailwindcss
。
? Which template do you want to use? › - Use arrow-keys. Return to submit.
bare
hackernews
todomvc
with-auth
with-mdx
with-prisma
with-solid-styled
❯ with-tailwindcss
with-vitest
with-websocket
此应用程序将使用Typescript并将是服务器端渲染,因此请选择“是”。
✔ Which template do you want to use? › with-tailwindcss
✔ Server Side Rendering? … yes
✔ Use TypeScript? … yes
现在运行pnpm install
安装依赖项。当我们使用时,让我们安装我们将在项目中使用的其他依赖项。
pnpm install @urql/core cookie cookie-signature graphql graphql-tag
这些依赖关系与GraphQL和Cookie解析有关。我将向您展示以后使用的地方。
在继续之前,通过运行pnpm run dev
运行项目。如果您打开http://localhost:3000/,则应在浏览器中看到此。
风格身体
让我们将默认的背景颜色和文本颜色更改为其他东西。我们可以从src/root.tsx
做到这一点。
将以下类添加到<Body>
:
<Body class="bg-gray-900 text-gray-100 antialiased font-roboto">
删除默认的NAV栏链接
启动模板带有根组件中的导航链接列表。我们不需要它们来进行这个项目。因此,将其从src/root.tsx
中删除。
<!-- REMOVE THESE -->
<A class="mr-2" href="/">
Index
</A>
<A href="/about">About</A>
删除不需要的文件
对于此项目,我们不需要有关启动器模板随附的关于页面或计数器组件。因此,让我们删除src/routes/about.tsx
和src/components/Counter.tsx
。
将初始标记添加到主页
现在,让我们更新主页(src/routes/index.tsx
)以具有我们的应用程序的基本布局。
import { Component } from 'solid-js'
const Home: Component = () => {
return (
<div class="mt-20 w-full max-w-lg mx-auto">
<div>TODO: Header component</div>
<div class="mt-10 flex items-center justify-between">
<div>
Hey <span class="font-bold">TODO: USER NAME</span>
</div>
<form>
<button class="cursor-pointer text-gray-200 hover:text-white">
Log out
</button>
</form>
</div>
<div class="mt-2.5">
<div>TODO: AddTodoInput component</div>
<div class="mt-3.5">
<div>TODO: when loading show Spinner component</div>
<div>TODO: when todos are loaded, display them </div>
<div>TODO: If loaded todos are empty, display empty state</div>
</div>
</div>
</div>
)
}
export default Home
要知道我们将在此页面上拥有哪些组件,我用TODO: ...
标记了他们的位置。这样
创建标题组件
标题组件只是显示应用程序名称和SolidJS徽标的组件(作为一个不错的触摸ð)。
所以,在src/components
中创建Header.tsx
并将以下内容放入其中。
import { Component } from 'solid-js'
const Logo = () => {
return (
<>
{/* PASTE THE LOGO SVG HERE */}
{/* https://www.solidjs.com/assets/logo.123b04bc.svg */}
</>
)
}
const Header: Component = () => {
return (
<h1 class="text-4xl font-bold text-center flex items-center justify-center">
<div class="mr-2.5 w-10">
<Logo />
</div>
Todo App
</h1>
)
}
export default Header
现在让我们通过用<Header />
替换<div>TODO: Header component</div>
在主页上。
import { Component } from 'solid-js'
import Header from '~/components/Header'
const Home: Component = () => {
return (
<div class="mt-20 w-full max-w-lg mx-auto">
<Header />
{/* ... */}
添加登录页面
喜欢主页,我们将首先添加html,然后再用一些todo笔记。
要在SolidStart中添加一个新页面,只需将新组件添加到src/routes
文件夹中即可。让我们命名为login.tsx
。
import { Component } from 'solid-js'
import Header from '~/components/Header'
const Login: Component = () => {
return (
<div class="min-h-screen flex flex-col justify-center items-center">
<Header />
<div>TODO: Show an error if username or password is incorrect</div>
<form class="mt-5 w-full max-w-lg mx-auto flex flex-col">
<input
class="p-3.5 rounded-t border-b border-gray-300 text-gray-900 outline-none"
name="username"
type="text"
placeholder="Username"
required
/>
<input
class="p-3.5 rounded-b text-gray-900 outline-none"
name="password"
type="password"
placeholder="Password"
required
/>
<button class="mt-2.5 py-2.5 rounded bg-blue-500 hover:bg-blue-600">
Log in
</button>
</form>
<div class="mt-5">
Don't have an account?
<a class="text-blue-400 ml-1" href="/signup">
Sign up here
</a>
</div>
</div>
)
}
export default Login
如果您转到登录页面(http://localhost:3000/login),则应该看到以下内容:
添加注册页面
它应该类似于登录页面。
创建src/routes/signup.tsx
并列出以下内容:
import { Component } from 'solid-js'
import Header from '~/components/Header'
const Signup: Component = () => {
return (
<div class="min-h-screen flex flex-col justify-center items-center">
<Header />
<form class="mt-5 w-full max-w-lg mx-auto flex flex-col">
<input
class="p-3.5 rounded-t border-b border-gray-300 text-gray-900 outline-none"
name="name"
type="text"
placeholder="Name"
required
/>
<input
class="p-3.5 border-b border-gray-300 text-gray-900 outline-none"
name="username"
type="text"
placeholder="Username"
required
/>
<input
class="p-3.5 rounded-b text-gray-900 outline-none"
name="password"
type="password"
placeholder="Password"
required
/>
<button class="mt-2.5 py-2.5 rounded bg-blue-500 hover:bg-blue-600">
Sign up
</button>
</form>
<div class="mt-5">
Already have an account?
<a class="text-blue-400 ml-1" href="/login">
Log in here
</a>
</div>
</div>
)
}
export default Signup
如果您转到http://localhost:3000/signup,则应在浏览器中看到此内容:
使用<A />
使用客户端路由
现在,我们正在使用本机<a></a>
链接到其他页面。它可以正常工作,但是当用户单击它时,它会提出完整的请求(MPA风格)进入链接页面。
我们可以通过将其切换到增强的<A/>
版本将其重定向到客户端(SPA风格)上的页面。它使用户体验更快,更光滑。
用import { A } from 'solid-start'
导入后,您要做的就是将<a>
替换为<A>
。
所以,让我们更新使用它的登录页面和注册页面。
登录:
<A class="text-blue-400 ml-1" href="/signup">
Sign up here
</A>
注册:
<A class="text-blue-400 ml-1" href="/login">
Log in here
</A>
我们将如何实施身份验证?
身份验证是一个值得一系列博客文章的重要主题。但是要保持简单,我将说明它在这个应用程序中的工作原理,然后您可以自己理解代码中的详细信息(如果您想提高开发技能,我鼓励它)。
我们将使用JWT令牌进行身份验证。如果您不熟悉它,我建议在继续之前学习它。 (我在这里写了一篇博客文章:Understand JWT in 3 minutes。)
这是我们将如何处理此应用中的身份验证的快速概述:
- 用户通过提供其用户名和密码来登录
- 我们将用户名和密码发送到我们的GraphQl Server
- GraphQl检查它们是否有效(通过将它们与数据库中存储的内容进行比较)
- 那么,如果它们有效,则GraphQl Server会生成JWT令牌,并将其返回到响应中的应用
- 那么,我们将通过将其嵌入每个GraphQl查询和突变的标题来使用此令牌
现在这看起来很简单,但是当我们想将其存储在客户端时(因此用户不必每次关闭应用程序时)。
我们需要一个服务器和浏览器都可以访问令牌的地方。首先想到的是cookie,这是正确的。但是将其存储在常规的cookie中使我们解决了一些安全问题。避免这些问题的最佳方法是将其存储在仅HTTP的cookie中,这是只能从服务器访问的cookie。
现在的问题是:浏览器如何访问令牌?
记住,我们需要浏览器才能访问令牌以从浏览器发送查询和突变(即Ajax请求)。
答案是通过通过应用程序的API路由(由我们的应用程序的服务器运行)来代理浏览器请求。因此,我们没有将GraphQL请求直接发送到GraphQl Server URL,而是通过服务器上的内部端点发送它,该端点可以从Cookie访问令牌。然后将令牌取用,将其放在请求标题上,然后将请求转发到真实的GraphQl Server。
。可以轻松地使用SolidStart的API Routes创建内部服务器端点。
这是对该应用中的身份验证如何工作的简要说明。由于解释身份验证代码的工作方式超出了本教程的范围,我将向您展示如何添加身份验证代码以及如何使用它。
添加GraphQL客户端
您可以使用所需的任何GraphQL客户端。对于这个项目,我将使用URQL。
我们将在多个地方使用客户
让我们在src
中的名为lib
的文件夹中创建它。因此,创建src/lib/urqlClient.ts
并将以下内容放入其中(我将在下面解释它包含的内容)。
import { createClient } from '@urql/core'
import { isServer } from 'solid-js/web'
let token = ''
export function setToken(newToken: string) {
token = newToken
}
export const urqlClient = () => {
return createClient({
url: isServer ? import.meta.env.VITE_GRAPHQL_URL : '/api/graphql',
fetchOptions: () => {
return {
headers: {
authorization: token || ''
}
}
}
})
}
此模块导出两个功能:setToken
和urqlClient
。
在开始时,当应用程序加载时,将无法为客户端可用,但可以存储在cookie中。因此,我们需要一种设置它后的方法 - 这就是setToken
的目的。
urqlClient
用于获取GraphQl客户端的实例。它可以从浏览器或服务器中使用。如果它在服务器中使用,则我们需要将其URL直接设置为GraphQl Server(因为它无需在服务器中使用代理,因为它可以访问仅HTTP的cookie)。
请注意,我们如何使用import.meta.env.VITE_GRAPHQL_URL
从环境变量中获取URL,我们将存储在.env
中,我们将在一点点创建。
如果从浏览器中使用了GraphQL客户端,则我们应该将其URL设置为代理端点,在这种情况下,它将是/api/graphql
(您可以随意任命它,但必须匹配API路由的路径路由routes
文件夹)。
添加.env
将敏感数据存储为环境变量始终是一个好习惯。因此,让我们在根目录中创建.env
并将其放入其中:
VITE_GRAPHQL_URL=http://localhost:4000/graphql
VITE_COOKIE_TOKEN_KEY=solid-todoapp-token
VITE_COOKIE_SECRET_KEY=secret
我们可以轻松地使用import.meta.env.YOUR_VAR_NAME
在应用中的任何地方访问它们。
添加GraphQL代理API路由
由于我们使用了/api/graphql
作为代理URL,因此我们需要在src/routes/api/graphql.ts
中创建API路由。
这是要投入的代码。您不需要了解它的工作原理,而只知道它将从浏览器转发到真正的GrawsQl Server URL(将JWT令牌放在其标题上)。
import { APIEvent, json } from 'solid-start'
import cookieParser from 'cookie'
import cookieSign from 'cookie-signature'
async function parseBody(request: Request) {
const reader = request.body.getReader()
const stream = await new ReadableStream({
start(controller) {
return pump()
function pump() {
return reader.read().then(({ done, value }) => {
if (done) {
controller.close()
return
}
controller.enqueue(value)
return pump()
})
}
}
})
return await (await new Response(stream)).json()
}
function getToken(request: Request) {
const parsedCookies = cookieParser.parse(request.headers.get('Cookie'))
const unsigned = cookieSign.unsign(
parsedCookies[import.meta.env.VITE_COOKIE_TOKEN_KEY],
import.meta.env.VITE_COOKIE_SECRET_KEY
)
const decoded = JSON.parse(Buffer.from(unsigned, 'base64').toString('utf8'))
return decoded.token
}
const proxyGraphqlRequest = async (request: Request) => {
const payload = await parseBody(request)
const token = await getToken(request)
try {
const resposne = await fetch(import.meta.env.VITE_GRAPHQL_URL, {
method: 'POST',
mode: 'cors',
headers: {
...request.headers,
'content-type': 'application/json',
authorization: token
},
body: JSON.stringify(payload)
})
const jsonResponse = await resposne.json()
return json(jsonResponse)
} catch (error) {
console.log('error', error)
}
}
export const POST = ({ request }: APIEvent) => {
return proxyGraphqlRequest(request)
}
添加查询和突变
此应用程序的GraphQl Server为我们提供了一个查询和突变列表,我们将用于登录用户,注册用户,添加todos,Update Todos等。
。让我们根据src/graphql/
将它们添加到我们的项目中。所有查询都将在src/graphql/queries.ts
中进行,所有突变都将在src/graphql/mutations.ts
中进行。
将以下内容放入src/graphql/queries.ts
:
import { gql } from 'graphql-tag'
export const CURRENT_USER = gql`
query currentUser {
currentUser {
id
username
name
}
}
`
export const TODOS = gql`
query todos {
todos {
id
title
completed
}
}
`
将其纳入src/graphql/mutations.ts
:
import { gql } from 'graphql-tag'
export const SIGNUP_USER = gql`
mutation signupUser($input: SignupUserInput!) {
signupUser(input: $input) {
status
}
}
`
export const LOGIN_USER = gql`
mutation loginUser($username: String!, $password: String!) {
loginUser(username: $username, password: $password) {
token
}
}
`
export const UPDATE_TODO = gql`
mutation updateTodo($input: UpdateTodoInput) {
updateTodo(input: $input) {
status
}
}
`
export const DELETE_TODO = gql`
mutation deleteTodo($todoId: ID) {
deleteTodo(todoId: $todoId) {
status
}
}
`
export const ADD_TODO = gql`
mutation addTodo($input: AddTodoInput!) {
addTodo(input: $input) {
id
title
completed
}
}
`
在SOLIDSTART中的行动
solidstart为我们提供了处理表单提交的整洁方法。表单提交可以在浏览器端(AJAX)或服务器端完成。
什么决定使用哪个是您用于创建操作的函数。用于服务器提交的createServerAction$,浏览器提交的createRouteAction。
调用任何一个返回两个值的数组。第一个包含操作的状态(例如,它正在待处理,或者存在错误)。第二个值包含运行动作的函数。
为了使其变得更好,我们可以从第二个值中访问<Form />
,这是表单元素的增强版本。提交表单后,使用它将运行该操作。
集成注册表格
要集成注册表单,我们需要为服务器创建一个操作,以发送GraphQL突变以创建新用户。然后,我们将用我们从操作中获得的<Form />
组件替换当前的<form>
。
集成后的注册页面的完整代码:
import { Component } from 'solid-js'
import { A } from 'solid-start'
import { createServerAction$, redirect } from 'solid-start/server'
import Header from '~/components/Header'
import { SIGNUP_USER } from '~/graphql/mutations'
import { urqlClient } from '~/lib/urqlclient'
const Signup: Component = () => {
const [_, { Form }] = createServerAction$(async (form: FormData) => {
const name = form.get('name')
const username = form.get('username')
const password = form.get('password')
const result = await urqlClient()
.mutation(SIGNUP_USER, { input: { name, username, password } })
.toPromise()
if (result) {
return redirect('/login')
}
})
return (
<div class="min-h-screen flex flex-col justify-center items-center">
<Header />
<Form class="mt-5 w-full max-w-lg mx-auto flex flex-col">
<input
class="p-3.5 rounded-t border-b border-gray-300 text-gray-900 outline-none"
name="name"
type="text"
placeholder="Name"
required
/>
<input
class="p-3.5 border-b border-gray-300 text-gray-900 outline-none"
name="username"
type="text"
placeholder="Username"
required
/>
<input
class="p-3.5 rounded-b text-gray-900 outline-none"
name="password"
type="password"
placeholder="Password"
required
/>
<button class="mt-2.5 py-2.5 rounded bg-blue-500 hover:bg-blue-600">
Sign up
</button>
</Form>
<div class="mt-5">
Already have an account?
<A class="text-blue-400 ml-1" href="/login">
Log in here
</A>
</div>
</div>
)
}
export default Signup
集成登录表单
让我首先向您展示代码并在下面解释。
import { Component, Show } from 'solid-js'
import { A, FormError } from 'solid-start'
import { createServerAction$, redirect } from 'solid-start/server'
import Header from '~/components/Header'
import { login } from '~/session'
const Login: Component = () => {
const [loggingIn, { Form }] = createServerAction$(async (form: FormData) => {
const username = form.get('username')
const password = form.get('password')
if (typeof username !== 'string' || typeof password !== 'string') {
throw new FormError('Form data are not correct')
}
const headers = await login({ username, password })
if (headers) {
return redirect('/', { headers })
}
})
return (
<div class="min-h-screen flex flex-col justify-center items-center">
<Header />
<Show when={loggingIn.error}>
<div class="mb-3.5 px-3.5 py-2 rounded-lg bg-red-700 text-red-50 text-sm font-medium">
Your username or password is incorrect
</div>
</Show>
<Form class="mt-5 w-full max-w-lg mx-auto flex flex-col">
<input
class="p-3.5 rounded-t border-b border-gray-300 text-gray-900 outline-none"
name="username"
type="text"
placeholder="Username"
required
/>
<input
class="p-3.5 rounded-b text-gray-900 outline-none"
name="password"
type="password"
placeholder="Password"
required
/>
<button class="mt-2.5 py-2.5 rounded bg-blue-500 hover:bg-blue-600">
Log in
</button>
</Form>
<div class="mt-5">
Don't have an account?
<A class="text-blue-400 ml-1" href="/signup">
Sign up here
</A>
</div>
</div>
)
}
export default Login
我们创建了类似于注册页面的服务器操作,但是这里的主要区别是我们在此处没有使用GraphQl客户端。相反,我们正在从一个名为session.ts
的文件中调用login()
函数,我们尚未创建。
原因是因为登录涉及更多仅发送GraphQl请求的内容。例如,用户登录后,我们需要将令牌存储在cookie中。
我们还可以在session.ts
中添加与身份验证相关的其他内容,例如getUser
以获取当前的用户数据,以及logout
。
为了完成登录页面,创建src/session.ts
并将其放入其中。
import { createCookieSessionStorage, FormError, redirect } from 'solid-start'
import { LOGIN_USER } from './graphql/mutations'
import { CURRENT_USER } from './graphql/queries'
import { setToken, urqlClient } from './lib/urqlclient'
const storage = createCookieSessionStorage({
cookie: {
name: import.meta.env.VITE_COOKIE_TOKEN_KEY,
secure: true,
secrets: [import.meta.env.VITE_COOKIE_SECRET_KEY],
sameSite: 'lax',
path: '/',
maxAge: 60 * 60 * 24 * 30,
httpOnly: true
}
})
interface LoginForm {
username: string
password: string
}
export const login = async ({ username, password }: LoginForm) => {
const result = await urqlClient()
.mutation(LOGIN_USER, { username, password })
.toPromise()
if (!result.data?.loginUser?.token) {
throw new FormError('Wrong username or password')
}
const token = result.data.loginUser.token
const session = await storage.getSession()
session.set('token', token)
return {
'Set-Cookie': await storage.commitSession(session)
}
}
export const getUser = async (request: Request) => {
const session = await storage.getSession(request.headers.get('Cookie'))
const token = session.get('token')
if (!token) {
return null
}
setToken(token)
const result = await urqlClient().query(CURRENT_USER, {}).toPromise()
if (!result.data?.currentUser) {
return redirect('/login')
}
return result.data.currentUser
}
export const logout = async (request: Request) => {
const session = await storage.getSession(request.headers.get('Cookie'))
return redirect('/login', {
headers: {
'Set-Cookie': await storage.destroySession(session)
}
})
}
添加路线警卫
由路线警卫,我的意思是防止已登录的用户进入登录页和注册页面,并防止访客用户进入主页。
为此,我们需要使用src/sessions.ts
的getUser()
获取用户,并查看它是否返回用户数据。如果不是,则表示用户未登录。
要使重定向起作用,我们需要在页面渲染之前获取用户,换句话说,在服务器端获取用户。
我们可以在routeData中使用createServerData$轻松地做到这一点。
让我们在主页上使用它,以便将用户重定向到登录页面,如果尚未登录。
在const Home: Component = () => {
上方的src/routes/index.tsx
的顶部添加它:
import { Component } from 'solid-js'
import { redirect } from 'solid-start'
import { createServerData$ } from 'solid-start/server'
import Header from '~/components/Header'
import { getUser } from '~/session'
export function routeData() {
const user = createServerData$(async (_, { request }) => {
const user = await getUser(request)
if (!user) {
throw redirect('/login')
}
return user
})
return { user }
}
您现在可以在浏览器中对其进行测试。尝试转到http://localhost:3000,您应该自动重定向http://localhost:3000/login。
请注意,您应该首先登录,您可以通过清除cookie手动进行操作 - 我们稍后会实现注销按钮。
现在,让我们添加登录和注册路线警卫。
登录路线护罩类似于主页,除了我们登录用户时,我们重定向到主页。
更新src/routes/login.tsx
:
import { Component, Show } from 'solid-js'
import { A, FormError, useRouteData } from 'solid-start'
import {
createServerAction$,
createServerData$,
redirect
} from 'solid-start/server'
import Header from '~/components/Header'
import { getUser, login } from '~/session'
export function routeData() {
return createServerData$(async (_, { request }) => {
if (await getUser(request)) {
throw redirect('/')
}
return {}
})
}
const Login: Component = () => {
const user = useRouteData<typeof routeData>()
user()
// ...
这里要注意的一个重要的事情是,我们必须在组件开头调用user()
。我们需要这样做,因为路由数据在明确称为
让我们在注册页面中添加相同的东西。
更新src/routes/signup.tsx
:
import { Component } from 'solid-js'
import { A, useRouteData } from 'solid-start'
import {
createServerAction$,
createServerData$,
redirect
} from 'solid-start/server'
import Header from '~/components/Header'
import { urqlClient } from '~/lib/urqlclient'
import { getUser } from '~/session'
import { SIGNUP_USER } from '../graphql/mutations'
export function routeData() {
return createServerData$(async (_, { request }) => {
if (await getUser(request)) {
throw redirect('/')
}
return {}
})
}
const Signup: Component = () => {
const user = useRouteData<typeof routeData>()
user()
// ...
集成注销
我们在主页上有一个注销按钮,但它还没有任何作用。让我们通过添加一个从src/session.ts
调用logout()
的服务器操作来解决此问题。
所以,更新src/index.tsx
:
// ...
const Home: Component = () => {
const [, { Form }] = createServerAction$((f: FormData, { request }) =>
logout(request)
)
// ...
另外,更新以使用<Form />
的表单元素。
在主页上显示用户名
我们已经在主页上获取用户,但我们还没有使用它。
通过添加以下方式从路由获取用户数据:
const Home: Component = () => {
const { user } = useRouteData<typeof routeData>()
// ...
然后显示用户名代替TODO: USER NAME
:
<span class="font-bold">{user()?.name}</span>
创建旋转器组件
这是一个简单的SVG组件,我们将用于显示加载指示器。
创建src/components/Spinner.tsx
。
export default function Spinner() {
return (
<div class="w-full">
{/* By Sam Herbert (@sherb), for everyone. More @ http://goo.gl/7AJzbL */}
<svg
viewBox="0 0 38 38"
xmlns="http://www.w3.org/2000/svg"
stroke="currentColor"
>
<g fill="none" fill-rule="evenodd">
<g transform="translate(1 1)" stroke-width="2">
<circle stroke-opacity=".5" cx="18" cy="18" r="18" />
<path d="M36 18c0-9.94-8.06-18-18-18">
<animateTransform
attributeName="transform"
type="rotate"
from="0 18 18"
to="360 18 18"
dur="1s"
repeatCount="indefinite"
/>
</path>
</g>
</g>
</svg>
</div>
)
}
创建AddTodoInput组件
此组件将用于显示待办事项的输入,并使用ADD_TODO
突变创建烟道。
在src/components/AddTodoInput.tsx
上创建组件文件,然后将其添加到其中:
import { Component, createEffect, Show } from 'solid-js'
import { createRouteAction } from 'solid-start'
import { urqlClient } from '~/lib/urqlclient'
import Spinner from './Spinner'
import { ADD_TODO } from '../graphql/mutations'
interface AddTodoInputProps {
refetchTodos: () => void
}
const AddTodoInput: Component<AddTodoInputProps> = (
props: AddTodoInputProps
) => {
const [addingTodo, { Form }] = createRouteAction(async (form: FormData) => {
return await urqlClient()
.mutation(ADD_TODO, {
input: {
title: form.get('title')
}
})
.toPromise()
})
createEffect(() => {
if (addingTodo.result) {
props.refetchTodos()
}
})
let inputRef: HTMLInputElement
return (
<Form
class="px-5 w-full bg-gray-100 rounded-lg outline-none text-gray-900 flex items-center justify-between"
onSubmit={(e) => {
if (!inputRef.value.trim()) e.preventDefault()
setTimeout(() => (inputRef.value = ''))
}}
>
<input
class="rounded-lg py-3.5 flex-1 bg-gray-100 outline-none pr-2.5"
name="title"
type="text"
placeholder="What needs to be done?"
readonly={addingTodo.pending}
ref={inputRef}
/>
<Show when={addingTodo.pending}>
<div class="text-blue-900 w-8">
<Spinner />
</div>
</Show>
</Form>
)
}
export default AddTodoInput
这里要注意的两件事。
首先,我们正在使用createRouteAction
而不是createServerData$
发送突变,因为我们想从浏览器(ajax)发送它。
第二,我们接受了一个称为refetchTodos
的道具。在我们添加新的函数之后,这将是一种函数。我们将从主页组件中传递(我们会稍等一下)。
创建待办事项组件
此组件将用于在列表中显示每个待办事项。它还将包含用于切换其completed
状态的代码,以及删除自身的代码。
将使用UPDATE_TODO
突变进行切换。并使用DELETE_TODO
突变删除它。
此组件接受两个道具:todo
,它将包含要显示的todo数据和refetchTodos
,该数据将在Todos被切换或删除时取消。
这是托迪特部分组件(src/components/TodoItem.tsx
)的完整代码:
import { Component, createEffect, Show } from 'solid-js'
import { createRouteAction } from 'solid-start'
import { urqlClient } from '~/lib/urqlclient'
import { DELETE_TODO, UPDATE_TODO } from '../graphql/mutations'
export interface Todo {
id: string
title: string
completed: boolean
}
interface TodoProps {
todo: {
id: string
title: string
completed: boolean
}
refetchTodos: () => void
}
const TodoItem: Component<TodoProps> = (props: TodoProps) => {
const [deletingTodo, deleteTodo] = createRouteAction(
async (todoId: string) => {
return await urqlClient()
.mutation(DELETE_TODO, {
todoId: todoId
})
.toPromise()
}
)
const [togglingTodo, toggleTodo] = createRouteAction(
async ({ todoId, completed }: { todoId: string; completed: boolean }) => {
return await urqlClient()
.mutation(UPDATE_TODO, {
input: {
todoId,
completed
}
})
.toPromise()
}
)
createEffect(() => {
if (deletingTodo.result || togglingTodo.result) {
props.refetchTodos()
}
})
return (
<div class="group flex items-center justify-between bg-gray-200 rounded-lg px-5 py-3.5 text-gray-900">
<div class="flex items-center">
<button
class={
'hover:border-blue-500 border-2 w-5 h-5 rounded-full flex items-center justify-center text-white cursor-pointer ' +
(props.todo.completed
? 'bg-blue-500 border-blue-500'
: 'border-gray-500')
}
onClick={() =>
toggleTodo({
todoId: props.todo.id,
completed: !props.todo.completed
})
}
>
<Show when={props.todo.completed}>
<svg style="width: 15px; height: 15px" viewBox="0 0 24 24">
<path
fill="currentColor"
d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"
/>
</svg>
</Show>
</button>
<span
class={
(props.todo.completed ? 'line-through text-gray-500' : '') +
' ml-2.5'
}
>
{props.todo.title}
</span>
</div>
<button
class="opacity-0 group-hover:opacity-100 text-gray-500 hover:text-gray-900"
onClick={() => deleteTodo(props.todo.id)}
>
<svg style="width: 24px; height: 24px" viewBox="0 0 24 24">
<path
fill="currentColor"
d="M9,3V4H4V6H5V19A2,2 0 0,0 7,21H17A2,2 0 0,0 19,19V6H20V4H15V3H9M9,8H11V17H9V8M13,8H15V17H13V8Z"
/>
</svg>
</button>
</div>
)
}
export default TodoItem
全部获取
让我们在src/routes/index.tsx
中更新routeData
与用户一起获取Todos。
import { Component, createResource } from 'solid-js'
import { redirect, useRouteData } from 'solid-start'
import { createServerAction$, createServerData$ } from 'solid-start/server'
import Header from '~/components/Header'
import { Todo } from '~/components/TodoItem'
import { TODOS } from '~/graphql/queries'
import { urqlClient } from '~/lib/urqlclient'
import { getUser, logout } from '~/session'
export function routeData() {
const user = createServerData$(async (_, { request }) => {
const user = await getUser(request)
if (!user) {
throw redirect('/login')
}
return user
})
const [todos, { refetch }] = createResource<Todo[]>(async () => {
const { data } = await urqlClient().query(TODOS, {}).toPromise()
return data.todos
})
return { user, todos, refetchTodos: refetch }
}
请注意我们也如何从中返回refetchTodos
。
接下来,让我们更新组件以使用新字段。
const Home: Component = () => {
const { user, todos, refetchTodos } = useRouteData<typeof routeData>()
todos()
// ...
显示AddTodoInput组件
我们的下一步是使用我们已经创建的<AddTodoInput />
组件。
导入它,然后用:
替换<div>TODO: AddTodoInput component</div>
<AddTodoInput refetchTodos={refetchTodos} />
添加新的Todos应该有效,但是您不会看到添加的内容,因为我们尚未显示Todos。这是下一步。
所有显示
展示毒品有三个状态:加载托多斯,在待办事项清单为空或显示待办事项时显示空状态。
要显示加载指示器,请用:
替换<div>TODO: when loading show Spinner component</div>
<Show when={todos.loading}>
<div class="mt-10 flex items-center justify-center text-xl font-medium">
<div class="w-5 mr-2.5">
<Spinner />
</div>
Loading Todos
</div>
</Show>
当我们为用户提供戒酒时,请使用<TodoItem />
组件显示它们。因此,用:
替换<div>TODO: when todos are loaded, display them </div>
<Show when={!todos.loading && todos() && todos().length > 0}>
<div class="space-y-2.5">
<For each={todos()}>
{(todo: Todo) => (
<TodoItem todo={todo} refetchTodos={refetchTodos} />
)}
</For>
</div>
</Show>
对于空状态,用:
替换<div>TODO: If loaded todos are empty, display empty state</div>
<Show when={!todos.loading && !todos().length}>
<div class="mt-10 text-center text-blue-50 text-opacity-40">
Your Todo List is Empty
</div>
</Show>
该应用程序准备就绪
恭喜!您已经创建了一个使用SolidJ和SOCESTART的功能齐全的应用程序。
您应该能够注册一个新用户,登录应用程序,从应用程序登录,添加Todos,Toggle Todos和Delete Todos。
感谢您阅读â2k!在Twitter上关注我,以获取有关我最新的博客文章,视频教程和一些很酷的Web开发提示的更新。让我们成为朋友!