使用 Sapper 和 Prisma 创建一个问答应用程序。第 2 部分:前端





5.00/5 (1投票)
了解如何使用 Sapper、Svelte 和 Prisma 轻松创建单页应用程序
引言
在本文的第一部分中,我们探讨了全栈应用程序开发的问题,以及使用 Prisma 和 Sapper 作为潜在解决方案。
Sapper 是一个框架,允许使用 Svelte 编写不同大小的应用程序。它在内部启动一个服务器,该服务器分发经过优化且对 SEO 友好的 Svelte 应用程序。
Prisma 是一套用于使用 GraphQL 设计和处理关系数据库的实用程序。在第 1 部分中,我们部署了 Prisma 服务器的环境并将其与 Sapper 集成。
在第 2 部分中,我们将继续在 Svelte 和 Sapper 上开发问答应用程序代码,并将其与 Prisma 集成。
免责声明
我们有意不实现测验参与者的授权/注册,但我们假设这在某些情况下可能很重要。我们将使用会话而不是注册/授权客户端。为了实现会话支持,我们需要更新 src/server.js 文件。
为了顺利运行,我们还需要安装额外的依赖项
```bash
yarn add graphql-tag express-session node-fetch
```
我们将会话中间件添加到服务器。这必须在调用 Sapper 中间件之前完成。此外,sessionID
必须传递给 Sapper。在加载 Sapper 应用程序时将使用会话。对于 API 路由,可以取消会话处理。
```js
server.express.use((req, res, next) => {
if (/^\/(playground|graphql).*/.test(req.url)) {
return next();
}
session({
secret: 'keyboard cat',
saveUninitialized: true,
resave: false,
})(req, res, next)
});
server.express.use((req, res, next) => {
if (/^\/(playground|graphql).*/.test(req.url)) {
return next();
}
return sapper.middleware({
session: (req) => ({
id: req.sessionID
})
})(req, res, next);
});
```
在第 1 部分中,我们考虑了 Prisma 的数据模型,该模型必须为后续工作进行更新。最终版本如下
```graphql
type QuizVariant {
id: ID! @id
name: String!
value: Int!
}
type Quiz {
id: ID! @id
title: String!
participantCount: Int!
variants: [QuizVariant!]! @relation(link: TABLE, name: "QuizVariants")
}
type SessionResult {
id: ID! @id
sessionId: String! @id
quiz: Quiz!
variant: QuizVariant
}
```
在更新的数据模型中,测验结果转移到 QuizVariant
类型。SessionResult
类型存储特定会话测验中的结果。
接下来,我们需要使用服务器端渲染更新 src/client/apollo.js 文件。我们将 ApolloClient
链接转发到 fetch
函数,因为 fetch
在 Node.js 运行时中不可用。
```js
import ApolliClient from 'apollo-boost';
import fetch from "node-fetch";
const client = new ApolliClient({
uri: 'https://:3000/graphql',
fetch,
});
export default client;
```
由于这是一个前端应用程序,我们将使用通过 CDN 连接的 TailwindCSS
和 FontAwesome
来简化标记。
为了简化解析器逻辑的支持,我们将所有解析器转移到一个文件中 (src/resolvers/index.js)。
```js
export const quizzes = async (parent, args, ctx, info) => {
return await ctx.prisma.quizzes(args.where);
}
export default {
Query: {
quizzes,
},
Mutation: {}
}
```
要求
之前,我们在 GraphQL 模式中提供了应用程序数据模型。但它非常抽象,没有显示应用程序应该如何工作以及它由哪些屏幕和组件组成。为了继续,我们需要更详细地考虑这些问题。
应用程序的主要目的是进行测验。用户可以创建测验、通过测验、接收和分享结果。基于此功能,我们需要应用程序中的以下页面
- 主页,其中包含测验列表和每个测验的参与者数量
- 一个可以有两种状态(用户已投票/未投票)的测验页面
- 测验创建页面
让我们开始吧。
测验创建页面
我们需要创建测验以显示在主页上。因此,我们从计划的第三点开始。
在测验创建页面上,用户应该能够通过特殊表单输入答案选项和测验名称。
让我们组装将存储通过表单输入的数据的模型。我们将为此使用普通的 JavaScript。该模型由两个变量组成,newQuiz
和 newVariant
newQuiz
是发送到服务器以保存到数据库的object
newVariant
是存储新答案选项的string
```js
const newQuiz = {
title: "",
variants: []
};
let newVariant = "";
let canAddQuiz = false;
// a variable that is recalculated only if `newQuiz.variants` or `newQuiz.title` changes
$: canAddQuiz = newQuiz.title === "" || newQuiz.variants.length === 0;
```
接下来,我们创建两个函数,用于在测验模型中添加和删除答案选项。这些是常规的 JavaScript 函数,它们重新定义了 newQuiz
模型的 variants
字段。
```js
function addVariant() {
newQuiz.variants = [
...newQuiz.variants,
{ name: newVariant, value: 0 }
];
// after adding
newVariant = "";
}
function removeVariant(i) {
return () => {
newQuiz.variants = newQuiz.variants.slice(0, i).concat(newQuiz.variants.slice(i + 1))
};
}
```
这些函数根据一定的逻辑转换模型。但是表示层 (Svelte) 如何知道这些更改呢?
所有 Svelte 组件代码在组装过程中都通过了提前编译阶段,并被转译为优化的 JS。
Svelte 组件中的变量变为“响应式”,对变量的任何更改都不会影响直接值,而是为该值的更改创建事件。之后,所有依赖于更改变量的内容都会在 DOM 中重新计算或重新绘制。
下一步是将请求发送到 Prisma 服务器。此时,我们将使用之前定义的 apollo-client 实例。
```js
import gql from 'graphql-tag';
import { goto } from '@sapper/app';
import client from "../client/apollo";
// request template
const createQuizGQLTag = gql`
mutation CreateQuiz($data: QuizCreateInput!) {
createQuiz(data: $data) {
id
}
}
`;
async function publishQuiz() {
await client.mutate({
mutation: createQuizGQLTag,
variables: {
"data": {
"title": newQuiz.title,
"participantCount": 0,
"variants": {
"create": newQuiz.variants
},
"result": {}
}
}
});
goto("/");
}
```
为了向 GraphQL
服务器发送请求,我们定义了一个请求模板和一个在 GraphQL
服务器上引起突变的函数,使用请求模板和存储在 newQuiz
变量中的数据作为 query
变量。
如果我们现在尝试发送请求,尝试将失败,因为我们尚未在服务器上设置测验创建处理程序。
我们将 createQuiz
突变的解析器添加到 src/resolvers/index.js 中
```js
...
const createQuiz = async (parent, args, ctx) => {
return await ctx.prisma.createQuiz(args.data);
}
export default {
...
Mutation: {
createQuiz
}
}
```
接口已准备好正常工作,只需要创建表单的标记。
```svelte
<form class="bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4">
<div class="mb-4">
<label class="block text-gray-700 text-sm font-bold mb-2" for="quiz-title">
Quiz title
</label>
<input
class="shadow appearance-none border rounded w-full py-2
px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
id="quiz-title"
type="text"
placeholder="Quiz title"
bind:value={newQuiz.title}
>
</div>
<div class="mb-4">
<label class="block text-gray-700
text-sm font-bold mb-2" for="new-variant">
New Variant
</label>
<input
class="shadow appearance-none border rounded w-full py-2 px-3
text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
id="new-variant"
type="text"
placeholder="New Variant"
bind:value={newVariant}
>
</div>
<div class="mb-4">
<label class="block text-gray-700 text-sm font-bold mb-2">
Variants
</label>
<!-- We display a list of quiz options or
specify that options have not yet been added -->
{#each newQuiz.variants as { name }, i}
<li>
{name}
<i class="cursor-pointer fa fa-close" on:click={removeVariant(i)} />
</li>
{:else}
No options have been added
{/each}
</div>
<div class="flex items-center justify-between">
<button
class="bg-blue-500 hover:bg-blue-700 text-white font-bold
py-2 px-4 rounded focus:outline-none focus:shadow-outline"
type="button"
on:click={addVariant}
>
<i class="fa fa-plus" /> Add variant
</button>
<button
class="bg-green-500 hover:bg-blue-700 text-white font-bold
py-2 px-4 rounded focus:outline-none focus:shadow-outline"
type="button"
on:click={publishQuiz}
bind:disabled={canAddQuiz}
>
Create New
</button>
</div>
</form>
```
一般来说,这是使用 TailwindCSS
实用程序类命名空间的简单 HTML 标记。在此模板中,我们定义了 #each
迭代器,它渲染测验选项列表。如果列表为空,则会显示一条通知,说明尚未添加测验选项。
Svelte 使用 on:
任何属性来处理事件。在此模板中,我们处理点击添加选项按钮和测验创建按钮。addVariant
和 publishQuiz
函数分别作为处理程序。
在文本输入字段中,我们使用 bind:value
属性将组件的响应式变量与文本字段的值属性绑定。此外,bind:disabled
用于确定测验创建按钮的禁用状态。
现在,由于表单和代码处理事件已准备就绪,我们可以填写表单,单击按钮,然后将请求发送到 GraphQL 服务器。请求执行后,将重定向到主页并显示以下文本:Quizzes in Service: 1
。这意味着我们已成功添加测验。
作者注:在此示例中,我特意避免客户端验证,以免分散读者对文章主要主题的注意力。尽管如此,验证对于实际应用程序来说是必要且非常重要的。
主页
我们需要在主页上显示创建的测验。在大量请求的情况下,每个请求都需要很长时间才能处理,并且渲染速度远不如我们希望的那么快。因此,主页上应该使用分页。
这很容易在 GraphQL
请求中体现
```graphql
query GetQuizes($skip: Int, $limit: Int) {
quizzes(skip: $skip, first: $limit) {
id,
title,
participantCount
}
}
```
在此请求中,我们定义了两个变量:$skip
和 $limit
。它们被发送到 quizzes
请求。这些变量已在那里定义,在 `Prisma` 生成的方案中。
接下来,我们需要将这些变量传递给 src/resolvers/index.js 请求解析器。
```js
export const quizzes = async (parent, args, ctx) => {
return await ctx.prisma.quizzes(args);
}
```
现在可以从应用程序发出请求了。我们更新 `src/routes/index.svelte` 路由中的预加载逻辑。
```svelte
<script context="module">
import gql from 'graphql-tag';
import client from "../client/apollo";
const GraphQLQueryTag = gql`
query GetQuizes($skip: Int, $limit: Int) {
quizzes(skip: $skip, first: $limit) {
id,
title,
participantCount
}
}
`;
const PAGE_LIMIT = 10;
export async function preload({ query }) {
const { page } = query;
const queriedPage = page ? Number(page) : 0;
const {
data: { quizzes }
} = await client.query({ query: GraphQLQueryTag, variables: {
limit: PAGE_LIMIT,
skip: PAGE_LIMIT * Number(queriedPage)
}
});
return {
quizzes,
page: queriedPage,
};
}
</script>
```
在 preload
函数中,我们定义了一个带有 query
字段的参数。请求参数将发送到那里。我们可以通过 URL 以以下方式确定当前页面:https://:3000?Page=123。
页面限制可以通过覆盖 PAGE_LIMIT
变量来更改。
```svelte
<script>
export let quizzes;
export let page;
</script>
```
此页面的最后一步是标记描述。
```svelte
<svelte:head>
<title>Quiz app</title>
</svelte:head>
{#each quizzes as quiz, i}
<a href="quiz/{quiz.id}">
<div class="flex flex-wrap bg-white border-b border-blue-tial-100 p-5">
{i + 1 + (page * PAGE_LIMIT)}.
<span class="text-blue-500 hover:text-blue-800 ml-3 mr-3">
{quiz.title}
</span>
({quiz.participantCount})
</div>
</a>
{:else}
<div class="text-2xl">No quizzes was added :(</div>
<div class="mt-3">
<a
class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2
px-4 rounded focus:outline-none focus:shadow-outline"
href="create"
>
Create new Quiz
</a>
</div>
{/each}
{#if quizzes.length === PAGE_LIMIT}
<div class="mt-3">
<a
class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2
px-4 rounded focus:outline-none focus:shadow-outline"
href="?page={page + 1}"
>
more...
</a>
</div>
{/if}
```
页面之间的导航使用常规链接实现。唯一的区别是路由名称或参数被传递给 href
属性而不是源地址。
测验页面
现在我们需要开发应用程序中最重要的页面。测验页面必须提供答案选项。此外,已经通过测验的人必须显示结果和选定的选项。
遵循 Sapper 约定,我们创建 src/routes/quiz/[id].svelte 路由文件。
为了使测验正确显示,我们需要创建一个将返回的 GraphQL 请求
- 请求
- 测验每个选项的结果
- 如果客户端已经参与测验,则返回答案选项的
id
```svelte
<script context="module">
import gql from 'graphql-tag';
import client from "../../client/apollo";
const GraphQLQueryTag = gql`
query GetQuiz($id: ID!, $sessionId: String!) {
quiz(where: { id: $id }) {
id,
title,
participantCount,
variants {
id,
name,
value
}
}
sessionResults(where: { quiz: { id: $id }, sessionId: $sessionId}, first: 1) {
variant {
id
}
}
}
`;
export async function preload({ params }, session) {
const { id } = params;
const {
data: { quiz, sessionResults: [sessionResult] }
} = await client.query({ query: GraphQLQueryTag, variables: {
id,
sessionId: session.id
}
});
return {
id,
quiz,
sessionId: session.id,
sessionResult
}
}
</script>
```
结论
本文表明,Sapper、Svelte 和 Prisma 的组合可以快速进行全栈原型开发。使用 Sapper 框架和 Prisma,我们创建了一个功能齐全的问答应用程序,允许用户通过测验、接收和分享结果,以及创建自己的测验。
经过未来的改进,此应用程序可以支持实时机制,因为 Prisma 支持实时数据库更改通知。而这些并非该技术栈的所有可能性。
本文是与 Digital Clever Solutions 和 Opporty 的软件开发人员合作撰写的。
历史
- 2020 年 4 月 22 日:初始版本