Formulir feedback untuk website atau aplikasi

Formulir feedback untuk website atau aplikasi7 menit baca

Buat formulir feedback dalam aplikasi di SvelteKit

Formulir feedback dalam aplikasi SvelteKit: formulir di layout mengirim ke satu form action, yang menambahkan pengguna dari locals dan halaman dari request di server.

Preet SutharPendiri, Ikiform

Taruh formulir di layout yang membungkus layar untuk pengguna login dan kirim ke satu action bernama, /feedback?/send, dengan use:enhance. Hook handle menaruh pengguna di event.locals, sehingga action sendiri yang menambahkan ID dan email mereka serta membaca halaman dari request. Browser hanya mengirim skor, komentar, dan route id.

Tab SvelteKit di panduan utama mengirim JSON ke endpoint +server.ts dari halaman mana pun, login atau tidak. Halaman ini membangun versi untuk bagian dalam aplikasi: form action sungguhan yang bekerja sebelum JavaScript termuat, dengan identitas pengguna diambil dari sesi di server. Ditulis untuk SvelteKit 3 dan Svelte 5. Key dan allowed domains dibahas di panduan dokumentasi SvelteKit.

Mengapa satu action untuk setiap layar

Form action milik sebuah halaman, tetapi formulir feedback ada di layout dan tampil di puluhan halaman. Menyalin action ke setiap +page.server.ts akan berantakan. SvelteKit memungkinkan formulir memanggil action route lain dengan query string, jadi setiap layar mengirim ke /feedback?/send dan satu file memegang logikanya. Setiap nilai kemudian berasal dari tempat yang tepat:

NilaiSumberKepercayaan
rating, commentFormulirnyaApa yang ingin disampaikan pengguna
user_id, emailevent.locals.user, diatur oleh hook handleDibaca dari cookie sesi di server
pageReferer dari request actionHanya same-origin; selebihnya dibuang
screenpage.route.id di input tersembunyiLabel pengelompokan. Diperiksa terhadap pola sebelum disimpan

page.route.id adalah path file route, seperti /(app)/projects/[id]/settings. Semua halaman pengaturan proyek berbagi nilai ini, itulah yang Anda inginkan saat menghitung rata-rata skor per layar. Action menghapus segmen grup seperti (app), karena itu hanya detail susunan folder Anda.

Bagaimana kiriman feedback SvelteKit berjalan dari formulir layout melalui satu form actionformulirlayoutlayarmanapun,use:enhancehookhandlesesikelocals.useraction?/sendcekpenggunadanskorIkiformsecretkey,menyimpannyapage.formterimakasihdilayaryangsamafail(401atau400)pesandibawahformulirPOST /feedbacklocals+ pengguna, halamannavigate: falsetanpa pengguna atau skor burukBagaimana kiriman feedback SvelteKit berjalan dari formulir layout melalui satu form actionformulir layoutlayar mana pun,use:enhancehook handlesesi ke locals.useraction?/sendcek penggunadan skorIkiformsecret key,menyimpannyapage.formterima kasih di layaryang samafail(401atau 400)pesan dibawahformulirPOST /feedbacklocals+ pengguna, halamannavigate: falsetanpa pengguna atau skor buruk
  1. Pengguna menilai layar di formulir layout. use:enhance mengirimnya ke /feedback?/send tanpa memuat halaman.
  2. Hook handle berjalan lebih dulu, membaca cookie sesi, dan menaruh pengguna di event.locals.
  3. Action send memeriksa bahwa ada pengguna dan skor bulat dari 1 sampai 5. Jika tidak, action mengembalikan fail dengan pesan.
  4. Action menambahkan ID dan email pengguna, halaman dari Referer, dan route id yang sudah dibersihkan, lalu mengirim dengan secret key.
  5. update dengan navigate diatur ke false menerapkan hasilnya ke layar saat ini, jadi baris terima kasih tampil di tempat pengguna berada.
Tanpa JavaScript, browser mendarat di /feedback, yang menampilkan hasil yang sama dan tautan kembali.

Setup proyek

Struktur proyek
src/
  app.d.ts
  env.ts
  hooks.server.ts
  lib/
    FeedbackForm.svelte
    server/session.ts
  routes/
    (app)/+layout.svelte
    feedback/
      +page.server.ts
      +page.svelte
.env

Buat secret key dengan scope submissions:write di API keys. SvelteKit 3 mendeklarasikan environment variable di src/env.ts. Variabel bersifat privat dan dibaca saat runtime kecuali Anda menyatakan lain, jadi key tidak pernah sampai ke bundle browser dan bisa diubah di host tanpa rebuild. .env mengisinya selama pengembangan.

.env
IKIFORM_SECRET_KEY=ik_sk_your_secret_key
src/env.ts
import { defineEnvVars } from "@sveltejs/kit/env";

export const variables = defineEnvVars({
  IKIFORM_SECRET_KEY: {},
});

Tempel definisi ini di tab Fields formulir. Formulir menampilkan skor dan komentar; action menyediakan sisanya, dan field email membuat Reply di setiap notifikasi menuju ke pengguna.

Pratinjau

Menjalankan validator Ikiform di browser Anda. Tidak ada yang dikirim.

Bagaimana layar ini bekerja untuk Anda?

Diatur oleh server Anda dari request.

Kirim skor 6 atau field yang tidak ada di definisi, dan preview menampilkan 422 yang diubah action menjadi pesan.

Bangun

Taruh pengguna di locals

handle berjalan untuk setiap request yang diterima server, termasuk kiriman form action. Beri tipe pengguna sekali di app.d.ts, lalu baca dari sesi Anda di hook. readSession mewakili apa pun yang sudah dilakukan kode auth Anda dengan cookie.

src/app.d.ts
declare global {
  namespace App {
    interface Locals {
      user: { id: string; email: string } | null;
    }
  }
}

export {};
src/hooks.server.ts
import type { Handle } from "@sveltejs/kit/hooks";
import { readSession } from "#lib/server/session.js";

export const handle: Handle = async ({ event, resolve }) => {
  event.locals.user = await readSession(event.cookies.get("session"));
  return resolve(event);
};

Form action

src/routes/feedback/+page.server.ts
import { fail } from "@sveltejs/kit";
import { IKIFORM_SECRET_KEY } from "$app/env/private";
import type { Actions } from "./$types";

export const prerender = false;

const SCREEN = /^[a-z0-9/[\]._-]{1,120}$/;

function pageFrom(referer: string | null, origin: string) {
  try {
    const url = new URL(referer ?? "");
    return url.origin === origin ? url.origin + url.pathname : "";
  } catch {
    return "";
  }
}

export const actions = {
  send: async ({ request, locals, url }) => {
    const data = await request.formData();
    if (!locals.user) return fail(401, { message: "Your session ended. Sign in again, then send it." });

    const rating = Number(data.get("rating"));
    if (!Number.isInteger(rating) || rating < 1 || rating > 5) {
      return fail(400, { message: "Pick a score from 1 to 5." });
    }

    const screen = String(data.get("screen") ?? "").replace(/\/\([^)]+\)/g, "") || "/";
    const back = pageFrom(request.headers.get("referer"), url.origin);

    let response: Response;
    try {
      response = await fetch("https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx", {
        method: "POST",
        headers: {
          Accept: "application/json",
          "Content-Type": "application/json",
          Authorization: `Bearer ${IKIFORM_SECRET_KEY}`,
          Origin: request.headers.get("origin") ?? url.origin,
        },
        body: JSON.stringify({
          rating,
          comment: String(data.get("comment") ?? "").trim().slice(0, 4000),
          page: back,
          screen: SCREEN.test(screen) ? screen : "unknown",
          user_id: locals.user.id,
          email: locals.user.email,
          _gotcha: String(data.get("_gotcha") ?? ""),
        }),
        signal: AbortSignal.timeout(8000),
      });
    } catch {
      return fail(502, { message: "We couldn't send it. Try again in a moment." });
    }

    if (response.status === 429) return fail(429, { message: "Too many answers at once. Try again in a minute." });
    const result = await response.json().catch(() => null);
    if (!result?.ok) {
      return fail(400, { message: result?.error?.fields?.comment?.[0] ?? result?.error?.message ?? "We couldn't send it." });
    }
    return { sent: true, back };
  },
} satisfies Actions;
  • fail mengembalikan status dan data tanpa melempar exception. Di SvelteKit 3, formulir yang di-enhance juga menerima status itu, jadi 401 dan 400 muncul di tab network sebagaimana adanya.
  • pageFrom hanya menyimpan Referer yang berasal dari origin Anda sendiri, dan membuang query string-nya.
  • export const prerender = false sengaja ada di sana. Halaman dengan action tidak bisa di-prerender, jadi jika root layout Anda melakukan prerender secara default, build gagal tanpanya.
  • Baris Origin meneruskan origin browser, dengan fallback ke origin aplikasi sendiri, dan memberi tahu Ikiform dari mana request berasal. Itulah yang membuat uji lokal dihitung sebagai localhost, di bawah.
  • 429 dari Ikiform punya pesan sendiri, karena di balik server artinya formulir secara keseluruhan sedang sibuk, bukan pengguna ini.

Formulir layout

src/lib/FeedbackForm.svelte
<script lang="ts">
  import { enhance } from "$app/forms";
  import { page } from "$app/state";

  const scores = ["Broken", "Frustrating", "Fine", "Good", "Great"];
  let sending = $state(false);
</script>

{#if page.form?.sent}
  <p role="status">Thanks. We read every one.</p>
{:else}
  <form
    method="POST"
    action="/feedback?/send"
    aria-busy={sending}
    use:enhance={() => {
      sending = true;
      return async ({ update }) => {
        await update({ navigate: false, refreshAll: false });
        sending = false;
      };
    }}
  >
    <fieldset>
      <legend>How is this screen working for you?</legend>
      {#each scores as label, index}
        <label><input type="radio" name="rating" value={index + 1} required /> {label}</label>
      {/each}
    </fieldset>
    <label for="feedback-comment">What would make it better?</label>
    <textarea id="feedback-comment" name="comment" rows="4" maxlength="4000"></textarea>
    <input type="hidden" name="screen" value={page.route.id ?? ""} />
    <input name="_gotcha" type="text" tabindex="-1" autocomplete="off" hidden />
    {#if page.form?.message}
      <p role="alert">{page.form.message}</p>
    {/if}
    <button disabled={sending}>{sending ? "Sending" : "Send feedback"}</button>
  </form>
{/if}
  • Secara default, formulir yang di-enhance dengan action di halaman lain akan bernavigasi ke sana setelah kiriman. navigate: false menjaga pengguna tetap di layarnya dan menaruh hasilnya di page.form di sini.
  • refreshAll: false melewati menjalankan ulang setiap fungsi load setelah berhasil. Feedback tidak mengubah apa pun yang ditampilkan halaman.
  • Saat gagal, formulir tidak direset, jadi komentar tetap ada di kotak di samping pesannya.
  • page.form milik halaman saat ini. Saat pengguna bernavigasi, nilainya dikosongkan, dan layar berikutnya menampilkan formulir baru.
src/routes/(app)/+layout.svelte
<script lang="ts">
  import FeedbackForm from "#lib/FeedbackForm.svelte";

  let { children } = $props();
</script>

<main>{@render children()}</main>
<aside>
  <FeedbackForm />
</aside>

Tanpa JavaScript, formulir yang sama tetap bekerja. Browser mengirim ke /feedback?/send dan mendarat di /feedback, yang merender hasil dan tautan kembali ke halaman asalnya:

src/routes/feedback/+page.svelte
<script lang="ts">
  import type { PageProps } from "./$types";

  let { form }: PageProps = $props();
</script>

{#if form?.sent}
  <p role="status">Thanks. We read every one.</p>
  <a href={form.back || "/"}>Back to where you were</a>
{:else if form?.message}
  <p role="alert">{form.message}</p>
  <a href="/">Back to the app</a>
{:else}
  <p>Send feedback from the panel on any screen of the app.</p>
{/if}

Honeypot, pemeriksaan manusia, dan batas

Input tersembunyi _gotcha diteruskan tanpa diubah, dan Ikiform menyimpan yang terisi sebagai spam. Pemeriksaan manusia Ikiform dilewati untuk kiriman dengan secret key. Di sini action sudah menolak siapa pun tanpa sesi, yang menahan bot yang akan dihentikan pemeriksaan manusia. Yang tersisa adalah akun yang login mengirim terus-menerus, jadi atur preset rate limit untuk formulir yang mengirim dari satu server.

Versi sisi server mengirim dengan secret key, sehingga Ikiform melewati Allowed domains dan pemeriksaan manusianya. Semua kiriman lalu datang dari IP server Anda, dan batas Standard 5 per menit per pengunjung menghitung semua pengunjung Anda sebagai satu. Pilih rate limit Relaxed atau Custom untuk formulir yang Anda kirim lewat server, dan simpan IKIFORM_SECRET_KEY di environment variable khusus server.

Rendering dan deploy

Layar di sekitar formulir bisa dirender di server atau di-prerender sesuka Anda; formulir sendiri tidak butuh apa pun saat render. Hanya /feedback yang harus berjalan di server. Adapter diatur di vite.config.ts, di dalam plugin sveltekit().

HostAdapter dan key
Vercel@sveltejs/adapter-vercel. Tambahkan IKIFORM_SECRET_KEY di Environment Variables. Edge runtime sudah tidak ada di SvelteKit 3, jadi action berjalan di Node function.
Netlify@sveltejs/adapter-netlify. Atur key di environment variable situs.
Cloudflare@sveltejs/adapter-cloudflare dengan Wrangler 4.118 atau lebih baru, peer dependency-nya. Simpan key sebagai secret Worker, misalnya dengan wrangler secret put IKIFORM_SECRET_KEY, dan baca lewat $app/env/private seperti di atas.

Uji gratis di localhost

Tanpa paket, workspace menerima 50 kiriman uji per bulan dari localhost. Ikiform menentukannya dari header Origin, dan fetch dari server Anda tidak mengirimnya kecuali Anda mengaturnya. Itulah sebabnya action meneruskan Origin dari request: di vite dev nilainya http://localhost:5173, dan kiriman dihitung sebagai uji gratis. Hilangkan baris itu dan uji lokal kembali dengan 402 plan_required, ditahan sampai Anda memilih paket.

Pastikan berfungsi

  1. Jalankan vite dev, login, buka layar pengaturan sebuah proyek, dan kirim skor 2 dengan komentar.
  2. Buka Submissions. screen berisi /projects/[id]/settings, page berisi ID proyek asli, serta user_id dan email cocok dengan akun Anda.
  3. Hapus cookie sesi dan kirim lagi. Formulir menampilkan pesan login dan tetap menyimpan komentar Anda.
  4. Matikan JavaScript lalu kirim. Anda mendarat di /feedback dengan baris terima kasih dan tautan kembali ke layar.
  5. Balas email notifikasi. Balasannya menuju pengguna yang login.

Hal yang sering menjebak

MasalahYang harus dilakukan
Ucapan terima kasih tampil di /feedback alih-alih di layar saat iniCallback enhance memanggil update() tanpa navigate: false, atau formulirnya tidak di-enhance. Periksa blok use:enhance.
locals.user selalu null di actionHook membaca nama cookie yang berbeda, atau path cookie tidak mencakup /feedback. SvelteKit 3 mengatur cookie baru ke / secara default, tetapi cookie yang diatur kode lama mungkin dibatasi ke subpath.
Kiriman dari domain lain milik Anda ditolakSvelteKit selalu memeriksa origin form post. Tambahkan origin itu ke csrf.trustedOrigins alih-alih membukanya untuk semua orang.
Build gagal dengan error tentang prerendering /feedbackHalaman dengan action tidak bisa di-prerender. Pertahankan export const prerender = false di +page.server.ts-nya.
422 validation_failedKiriman tidak cocok dengan definisi field formulir. error.fields menyebutkan setiap field dan alasannya. Samakan name input dengan ID field.

Pertanyaan

Mentok di salah satu langkah, atau ingin panduan untuk situasi Anda? Email hello@ikiform.com.