65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIconstarIcon

5.00/5 (1投票)

2020 年 4 月 22 日

CPOL

7分钟阅读

viewsIcon

6913

了解如何使用 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 连接的 TailwindCSSFontAwesome 来简化标记。

为了简化解析器逻辑的支持,我们将所有解析器转移到一个文件中 (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 模式中提供了应用程序数据模型。但它非常抽象,没有显示应用程序应该如何工作以及它由哪些屏幕和组件组成。为了继续,我们需要更详细地考虑这些问题。

应用程序的主要目的是进行测验。用户可以创建测验、通过测验、接收和分享结果。基于此功能,我们需要应用程序中的以下页面

  1. 主页,其中包含测验列表和每个测验的参与者数量
  2. 一个可以有两种状态(用户已投票/未投票)的测验页面
  3. 测验创建页面

让我们开始吧。

测验创建页面

我们需要创建测验以显示在主页上。因此,我们从计划的第三点开始。

在测验创建页面上,用户应该能够通过特殊表单输入答案选项和测验名称。

让我们组装将存储通过表单输入的数据的模型。我们将为此使用普通的 JavaScript。该模型由两个变量组成,newQuiznewVariant

  • 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: 任何属性来处理事件。在此模板中,我们处理点击添加选项按钮和测验创建按钮。addVariantpublishQuiz 函数分别作为处理程序。

在文本输入字段中,我们使用 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 SolutionsOpporty 的软件开发人员合作撰写的。

历史

  • 2020 年 4 月 22 日:初始版本
© . All rights reserved.