StrapiとSvelteによるInstagramクローンの構築(その3 )
100664 ワード
こんにちは!私は、このチュートリアルのパート3で戻ります!約束通り、この部分では、投稿を作成したり、コメントを追加したり、画像をアップロードするなどの認証要求を処理します.始めましょう!
私たちのアイコンは動作しません.彼らは、ちょうど箱を示します.これは、まだアイコンのフォントを追加していないためです.私たちはCSSを加えました、しかし、また、我々はフォントを入れる必要があります
我々がstrapiで認証するとき、我々はJWTトークンを取り戻します.このトークンは、その後、任意のメール/パスワードを送信することなく、他のリクエストで自分自身を認証するために使用することができます.更新しましょう
ヘルパーメソッドを使いましょう
ユーザーがログインしているならば、彼らは訪問します
ロギングアウトは非常に簡単です、我々もこれのためにstrapiに連絡する必要はありません!我々はちょうどローカルストレージからトークンを削除する必要があります.私はルートと呼ばれる
最初にコメントに集中しましょう
我々は不眠症でこれをしました、しかし、現在、我々のフロントエンドでそれをするコードを加えましょう.修正します
今すぐポストに同じ治療を与える!私は、Aをつくるつもりです
そして、それ!私たちはここで、ほとんどほぼ完了です.これは良いですが、我々はそれをより安全にすることができます.第4で、そして、最終的な部分で、私はあなたにHerokuとVercelの上でstrapiとフロントエンドの両方を展開する方法を示します.デモを見てみましょう.
strapiは本当に楽しかったですが、まだ私のカスタムメイドのバックエンドをミスします.私がStrapiが不足していると思う1つのことは、JWTからユーザ情報を取得する能力です.つまり、そのAPIを編集する必要がなくなります.ユーザIDを取得している方法は、非常に安全でない方法であり、生産のためのものではありません.あなたが第4部でそれをどのように確保するかをあなたに教えます!
If you get stuck, or just want the source code, it is available on Github
フィニッシュ
私たちのアイコンは動作しません.彼らは、ちょうど箱を示します.これは、まだアイコンのフォントを追加していないためです.私たちはCSSを加えました、しかし、また、我々はフォントを入れる必要があります
public/webfonts
. フォントをダウンロードCDNJS として保存public/webfonts/fa-solid-900.woff2
. あなたのアイコンが正常に表示されるはずです認証の取り扱い
我々がstrapiで認証するとき、我々はJWTトークンを取り戻します.このトークンは、その後、任意のメール/パスワードを送信することなく、他のリクエストで自分自身を認証するために使用することができます.更新しましょう
Auth.svelte
コンポーネント<!-- src/components/Auth.svelte -->
<script lang="ts">
import Error from "./ErrorAlert.svelte";
import { fade } from "svelte/transition";
import { getContext } from "svelte";
import axios from "axios";
type AuthMode = "login" | "register";
export let authMode: AuthMode = "register";
export let next: string = "/posts";
const apiUrl: string = getContext("apiUrl");
let loginError: string | null = null;
let registerError: string | null = null;
let email = "";
let password = "";
let cpassword = "";
let username = "";
function login() {
email = email.trim();
password = password.trim();
if (!email || !password) {
loginError = "Fill out all fields!";
return;
}
loginError = null;
axios
.post(apiUrl + "/auth/local", {
identifier: email,
password,
})
.then(({ data }) => {
localStorage.setItem("JWT", data.jwt);
localStorage.setItem("user", JSON.stringify(data.user));
// Using window.location.href instead of router.redirect to refresh the page
// so that components like Navbar update too
window.location.href = next;
})
.catch((err) => {
if (err.response) {
loginError = "";
for (let message of err.response.data.message[0].messages) {
loginError += `${message.message}\n`;
}
} else loginError = err;
});
}
function register() {
email = email.trim();
password = password.trim();
cpassword = cpassword.trim();
username = username.trim();
if (!email || !password || !cpassword || !username) {
registerError = "Fill out all fields!";
return;
}
if (password !== cpassword) {
registerError = "Passwords don't match";
return;
}
registerError = null;
axios
.post(apiUrl + "/auth/local/register", {
email,
username,
password,
})
.then(({ data }) => {
localStorage.setItem("JWT", data.jwt);
localStorage.setItem("user", JSON.stringify(data.user));
// Using window.location.href instead of router.redirect to refresh the page
// so that components like Navbar update too
window.location.href = next;
})
.catch((err) => {
if (err.response) {
registerError = "";
for (let message of err.response.data.message[0].messages) {
registerError += `${message.message}\n`;
}
} else registerError = err;
});
}
</script>
<style>
.auth-box {
width: 40%;
margin: 1rem auto;
}
@media (max-width: 600px) {
.auth-box {
width: 80%;
}
}
</style>
<div class="w3-container">
<div class="w3-card-4 w3-border w3-border-black auth-box">
<div class="w3-bar w3-border-bottom w3-border-gray">
<button
style="width: 50%"
on:click={() => (authMode = 'login')}
class="w3-bar-item w3-button w3-{authMode === 'login' ? 'blue' : 'white'} w3-hover-{authMode === 'login' ? 'blue' : 'light-gray'}">Login</button>
<button
style="width: 50%"
on:click={() => (authMode = 'register')}
class="w3-bar-item w3-button w3-{authMode === 'register' ? 'blue' : 'white'} w3-hover-{authMode === 'register' ? 'blue' : 'light-gray'}">Register</button>
</div>
<div class="w3-container">
<h3>{authMode === 'login' ? 'Login' : 'Register'}</h3>
{#if authMode === 'login'}
<form on:submit|preventDefault={login} in:fade>
{#if loginError}
<Error message={loginError} />
{/if}
<div class="w3-section">
<label for="email">Email</label>
<input
type="email"
bind:value={email}
placeholder="Enter your email"
id="email"
class="w3-input w3-border w3-border-black" />
</div>
<div class="w3-section">
<label for="password">Password</label>
<input
type="password"
bind:value={password}
placeholder="Enter your password"
id="password"
class="w3-input w3-border w3-border-black" />
</div>
<div class="w3-section">
<button
class="w3-button w3-blue w3-hover-blue w3-border w3-border-blue">Login</button>
<button
class="w3-button w3-white w3-hover-light-gray w3-border w3-border-black"
on:click={() => (authMode = 'register')}>Register</button>
</div>
</form>
{:else}
<form on:submit|preventDefault={register} in:fade>
{#if registerError}
<Error message={registerError} />
{/if}
<div class="w3-section">
<label for="username">Username</label>
<input
type="text"
bind:value={username}
placeholder="Enter a username"
id="username"
class="w3-input w3-border w3-border-black" />
</div>
<div class="w3-section">
<label for="email">Email</label>
<input
type="email"
bind:value={email}
placeholder="Enter your email"
id="email"
class="w3-input w3-border w3-border-black" />
</div>
<div class="w3-section">
<label for="password">Password</label>
<input
type="password"
bind:value={password}
placeholder="Enter a password"
id="password"
class="w3-input w3-border w3-border-black" />
</div>
<div class="w3-section">
<label for="cpassword">Confirm Password</label>
<input
type="password"
bind:value={cpassword}
placeholder="Re-enter that password"
id="cpassword"
class="w3-input w3-border w3-border-black" />
</div>
<div class="w3-section">
<button
class="w3-button w3-blue w3-hover-blue w3-border w3-border-blue">Register</button>
<button
class="w3-button w3-white w3-hover-light-gray w3-border w3-border-black"
on:click={() => (authMode = 'login')}>Login</button>
</div>
</form>
{/if}
</div>
</div>
</div>
さて、我々のアプリは、stradサーバーに移動し、JWTトークンを取得し、後で使用するために私たちのローカルストレージにそれを置く.つくりましょうsrc/auth.ts
トークンの取得および削除にはヘルパー関数があります.// src/auth.ts
import type { User } from "./types";
export function getToken(): string | null {
return localStorage.getItem("JWT") || null;
}
export function clearToken() {
localStorage.removeItem("JWT");
}
export function getUserId(): number | null {
let user: string | User = localStorage.getItem("user");
if (!user) return null;
user = JSON.parse(user);
return (user as User).id;
}
export function getUser(): User | null {
let user: string | User = localStorage.getItem("user");
if (!user) return null;
user = JSON.parse(user);
return user as User;
}
Navbarにおける条件付きレンダリング
ヘルパーメソッドを使いましょう
auth.ts
ログインしているかどうかを調べます.ユーザーがログインしているならば、私たちはアップロード・ボタン(私が「新しいポスト」に改名された)を示すだけであるためにこれをNavbarで使うことができます:<!-- src/components/Navbar.svelte -->
<script lang="ts">
import { slide } from "svelte/transition";
import { getToken } from "../auth";
const auth = !!getToken();
let active = false;
</script>
<style>
.toggler {
display: none;
}
@media (max-width: 600px) {
.logo {
display: block;
width: 100%;
}
.logo .toggler {
float: right;
display: initial;
}
.nav {
display: flex;
width: 100%;
flex-direction: column;
}
.nav a {
text-align: left;
}
}
</style>
<div class="w3-bar w3-blue">
<div class="logo">
<a
href="/"
class="w3-bar-item w3-text-white w3-button w3-hover-blue">Quickstagram</a>
<button
class="toggler w3-button w3-blue w3-hover-blue"
on:click={() => (active = !active)}>
<i class="fas fa-{active ? 'times' : 'bars'}" /></button>
</div>
<div class="w3-right w3-hide-small">
{#if auth}
<a href="/new" class="w3-bar-item w3-button w3-hover-blue">New post</a>
<a
href="/logout"
class="w3-bar-item w3-button w3-hover-blue">Logout</a>
{:else}
<a
href="/auth?action=login"
class="w3-bar-item w3-button w3-hover-blue">Login</a>
<a
href="/auth?action=register"
class="w3-bar-item w3-button w3-purple w3-hover-purple">Register</a>
{/if}
</div>
{#if active}
<div class="w3-right nav w3-hide-large w3-hide-medium" transition:slide>
{#if auth}
<a href="/new" class="w3-bar-item w3-button w3-hover-blue">New
post</a>
<a
href="/logout"
class="w3-bar-item w3-button w3-hover-blue">Logout</a>
{:else}
<a
href="/auth?action=login"
class="w3-bar-item w3-button w3-hover-blue">Login</a>
<a
href="/auth?action=register"
class="w3-bar-item w3-button w3-purple w3-hover-purple">Register</a>
{/if}
</div>
{/if}
</div>
自動リダイレクト
ユーザーがログインしているならば、彼らは訪問します
/auth
, 彼らは再びログインできます.これを防ぐために、自動的に/posts
. 両方でこれをしますauth.svelte
and index.svelte
<!-- src/routes/auth.svelte -->
<script lang="ts">
import Auth from "../components/Auth.svelte";
import router from "page";
import { onMount } from "svelte";
import { getToken } from "../auth";
export const params = {};
export let queryString: { action: "login" | "register"; next: string };
onMount(() => {
if (getToken()) router.redirect(queryString.next || "/posts");
});
</script>
<Auth authMode={queryString.action} next={queryString.next} />
<!-- src/routes/index.svelte -->
<script lang="ts">
import { onMount } from "svelte";
import { getToken } from "../auth";
import router from "page";
import Auth from "../components/Auth.svelte";
export const queryString = {};
export const params = {};
onMount(() => {
if (getToken()) router.redirect("/posts");
});
</script>
<div class="w3-container">
<h1 class="w3-center w3-xxxlarge">Quickstagram</h1>
<p class="w3-center w3-large w3-text-gray">Instagram, but quicker!</p>
<div class="w3-center">
<a
href="/auth?action=register"
class="w3-button w3-blue w3-border w3-border-blue w3-hover-blue">Register</a>
<a
href="/auth?action=login"
class="w3-button w3-white w3-border w3-border-black w3-hover-white">Login</a>
</div>
<Auth />
<div class="w3-center">
<a
href="/posts"
class="w3-button w3-blue w3-border w3-border-blue w3-hover-blue">View
posts</a>
</div>
</div>
ロギングアウト
ロギングアウトは非常に簡単です、我々もこれのためにstrapiに連絡する必要はありません!我々はちょうどローカルストレージからトークンを削除する必要があります.私はルートと呼ばれる
/logout
では、<!-- src/routes/logout.svelte -->
<script lang="ts">
import { onMount } from "svelte";
import { clearToken } from "../auth";
export let queryString: { next: string };
onMount(() => {
clearToken();
// Using window.location.href instead of router.redirect to refresh the page
// so that components like Navbar update too
window.location.href = queryString.next || "/";
});
</script>
<h1 class="w3-center w3-xxlarge">Logging you out...</h1>
そして、すべてのルートと同様に、我々は2009年にそれを登録する必要がありますApp.svelte
.<!-- src/App.svelte -->
<script lang="ts">
// ...
import Logout from "./routes/logout.svelte";
// ...
router("/logout", setupRouteParams, () => (page = Logout));
// ...
</script>
<!-- ... -->
コメントの追加
最初にコメントに集中しましょう
src/components/onePost.svelte
, コメントを追加できるようにする入力を追加しましょう.<!-- src/components/onePost.svelte -->
<script lang="ts">
import axios from "axios";
import { getContext } from "svelte";
import router from "page";
import { getToken } from "../auth";
import type { Post, Comment as CommentType } from "../types";
import Comment from "../components/Comment.svelte";
import ErrorAlert from "../components/ErrorAlert.svelte";
export let params: { username: string; postId: string };
const apiUrl = getContext("apiUrl");
const auth = !!getToken();
let commentError: string | null = null;
async function getPost(): Promise<Post> {
try {
const { data } = await axios.get<Post>(
apiUrl + "/posts/" + params.postId
);
if (data.user)
if (data.user.username !== params.username)
router.redirect("/404");
return data;
} catch (err) {
if (err.response.status === 404) router.redirect("/404");
else {
console.log({ error: err });
throw new Error(
"Request failed with status: " +
err.response.status +
"\nCheck the console for further details."
);
}
}
}
async function getComments(post: Post): Promise<CommentType[]> {
try {
let comments: CommentType[] = [];
for (let i = 0; i < post.comments.length; i++) {
const { data } = await axios.get<CommentType>(
apiUrl + "/comments/" + post.comments[i].id
);
comments.push(data);
}
return comments;
} catch (err) {
if (err.response) {
console.log({ err });
if (err.response.status === 404) router.redirect("/404");
else {
console.log({ error: err });
throw new Error(
"Request failed with status: " +
err.response.status +
"\nCheck the console for further details."
);
}
} else throw new Error(err);
}
}
function newComment() {}
</script>
<style>
#comment-form {
display: grid;
grid-template-rows: auto;
grid-template-columns: 80% 20%;
margin: 1rem 0;
}
.post {
width: 50%;
margin: 0 auto;
}
@media (max-width: 992px) {
.post {
width: 70%;
}
}
@media (max-width: 600px) {
.post {
width: 90%;
}
}
</style>
{#await getPost()}
<div class="w3-center w3-section w3-xxlarge w3-spin">
<i class="fas fa-spinner" />
</div>
{:then post}
<div class="w3-card post">
<a
href={post.image[0].provider === 'local' && getContext('apiUrl') + post.image[0].url}><img
src={post.image[0].provider === 'local' && getContext('apiUrl') + post.image[0].url}
alt={post.image.alternativeText || 'Post image'}
style="width: 100%" /></a>
<div class="w3-container">
<p class="w3-small w3-text-gray">
<a
href="/@{post.user.username}"
style="text-decoration: none">@{post.user.username}</a>
</p>
<p>{post.content}</p>
</div>
</div>
<div class="w3-card post w3-margin-top">
<header class="w3-container w3-border-bottom w3-border-light-gray">
<h3>Comments</h3>
</header>
<div class="w3-container">
{#if auth}
{#if commentError}
<ErrorAlert message={commentError} />
{/if}
<form on:submit|preventDefault={newComment} id="comment-form">
<input
type="text"
class="w3-input w3-border"
placeholder="Type your comment here"
id="comment" />
<button
class="w3-button w3-blue w3-hover-blue w3-border w3-border-blue"
type="submit">Add</button>
</form>
{/if}
{#await getComments(post)}
<div class="w3-center w3-section w3-xxlarge w3-spin">
<i class="fas fa-spinner" />
</div>
{:then comments}
{#each comments as comment}
<Comment {comment} />
{/each}
{:catch err}
<div
class="w3-panel w3-pale-red w3-padding w3-leftbar w3-border-red w3-text-red">
{err}
</div>
{/await}
</div>
</div>
{:catch err}
<div
class="w3-panel w3-pale-red w3-padding w3-leftbar w3-border-red w3-text-red">
{err}
</div>
{/await}
コメントを追加する不眠症を使用しましょうNote that we have to specify the
post
anduser
id too. Automatically determining them is not possible unless we edit Strapi's code. Also, you need to provide a JWT to authenticate the request.
我々は不眠症でこれをしました、しかし、現在、我々のフロントエンドでそれをするコードを加えましょう.修正します
newComment
ファンクションonePost.svelte
// src/routes/onePost.svelte
// script tag
function newComment(postId: number) {
// "as HTMLInputElement" is supported in TypeScript only.
const userId: number | null = getUserId();
if (!userId) {
window.location.href =
"/auth?action=login&next=" + window.location.pathname;
return;
}
const content = (
(document.getElementById("comment") as HTMLInputElement).value || ""
).trim();
if (!content) return;
axios
.post<Comment>(
apiUrl + "/comments",
{
content,
post: postId,
user: userId,
},
{
headers: {
Authorization: "Bearer " + getToken(),
},
}
)
.then(() => window.location.reload())
.catch((error) => {
if (error.response) {
if (
error.response.status === 401 ||
error.response.status === 403
)
window.location.href =
"/auth?action=login&next=" +
window.location.pathname;
else {
commentError = "";
for (let message of error.response.data.message[0]
.messages) {
commentError += `${message.message}\n`;
}
}
} else commentError = error;
});
}
テストしましょう!新規投稿の作成
今すぐポストに同じ治療を与える!私は、Aをつくるつもりです
/new
利用する路線newPost.svelte
.<!-- src/components/newPost.svelte -->
<script lang="ts">
import { getContext, onMount } from "svelte";
import { getToken, getUser } from "../auth";
import ErrorAlert from "../components/ErrorAlert.svelte";
import type { User } from "../types";
const apiUrl = getContext("apiUrl");
const user: User = getUser();
onMount(() => {
if (!getToken() || !user)
window.location.href =
"/auth?action=login&next=" + window.location.pathname;
});
let loading = false;
let error: string | null = null;
let file: File;
let content = "";
function chooseFile() {
const input = document.createElement("input");
input.type = "file";
input.accept = "image/*";
input.addEventListener("change", ({ target }) => {
if ((target as HTMLInputElement).files.length === 1) {
file = (target as HTMLInputElement).files[0];
}
});
input.click();
}
function newPost() {}
</script>
{#if user}
<h1 class="w3-center w3-xxxlarge">New post</h1>
<p class="w3-center w3-text-gray">Logged in as: {user.username}</p>
<div class="w3-card w3-margin">
{#if loading}
<div class="w3-center w3-container">
<i class="fas fa-spinner fa-spin fa-5x w3-margin" />
<p class="w3-xlarge">Uploading...</p>
</div>
{:else}
<form class="w3-container" on:submit|preventDefault={newPost}>
{#if error}
<ErrorAlert message={error} />
{/if}
<div class="w3-section">
{#if file}
<p>Chosen image: {file.name}</p>
{:else}
<button
type="button"
on:click={chooseFile}
class="w3-button w3-white w3-border">Choose image</button>
{/if}
</div>
<div class="w3-section">
<label for="content">Post content</label>
<textarea
id="content"
rows="5"
bind:value={content}
class="w3-input w3-border" />
</div>
<div class="w3-section">
<button
type="submit"
class="w3-button w3-blue w3-border w3-border-blue w3-hover-blue"
style="width: 100%">Post</button>
</div>
</form>
{/if}
</div>
{/if}
今、我々がする必要があるすべては、このイメージをアップロードし、新しいポストを作成します.エディットnewPost
ファンクションnewPost.svelte
<!-- src/routes/newPost.svelte -->
<script lang="ts">
// ...
function newPost() {
if (!content || !content.trim()) {
error = "Enter some content";
return;
}
if (!file) {
error = "Please choose a file";
return;
}
if (file.type.split("/")[0] !== "image") {
error = "Please choose an image";
return;
}
content = content.trim();
let fd = new FormData();
fd.append("files", file);
loading = true;
// uploading file
axios
.post<ImageType[]>(apiUrl + "/upload", fd, {
headers: {
"Content-Type": "multipart/formdata",
Authorization: "Bearer " + getToken(),
},
})
.then(({ data }) => {
const imageId: number = data[0].id;
// creating the post itself
axios
.post<Post>(
apiUrl + "/posts",
{
image: imageId,
user: getUserId(),
content,
},
{
headers: {
Authorization: "Bearer " + getToken(),
},
}
)
.then(({ data }) => {
window.location.href = `/@${data.user.username}/${data.id}`;
})
.catch((err) => {
if (err.response) {
if (
err.response.status === 401 ||
err.response.status === 400
)
window.location.href =
"/auth?action=login&next=" +
window.location.pathname;
else {
error = "";
for (let message of err.response.data.message[0]
.messages) {
error += `${message.message}\n`;
}
}
} else error = err;
});
})
.catch((err) => {
if (err.response) {
if (
err.response.status === 401 ||
err.response.status === 400
)
window.location.href =
"/auth?action=login&next=" +
window.location.pathname;
else {
error = "";
for (let message of err.response.data.message[0]
.messages) {
error += `${message.message}\n`;
}
}
} else error = err;
});
}
</script>
<!-- ... -->
Remember to import whenever required!
デモ
そして、それ!私たちはここで、ほとんどほぼ完了です.これは良いですが、我々はそれをより安全にすることができます.第4で、そして、最終的な部分で、私はあなたにHerokuとVercelの上でstrapiとフロントエンドの両方を展開する方法を示します.デモを見てみましょう.
結論
strapiは本当に楽しかったですが、まだ私のカスタムメイドのバックエンドをミスします.私がStrapiが不足していると思う1つのことは、JWTからユーザ情報を取得する能力です.つまり、そのAPIを編集する必要がなくなります.ユーザIDを取得している方法は、非常に安全でない方法であり、生産のためのものではありません.あなたが第4部でそれをどのように確保するかをあなたに教えます!
Reference
この問題について(StrapiとSvelteによるInstagramクローンの構築(その3 )), 我々は、より多くの情報をここで見つけました https://dev.to/arnu515/build-an-instagram-clone-with-strapi-and-svelte-part-3-23l2テキストは自由に共有またはコピーできます。ただし、このドキュメントのURLは参考URLとして残しておいてください。
Collection and Share based on the CC Protocol