[パート19 ][フロントエンド] GraphSQL、TypeScript、および反応を使用してTwitterのクローンを作成する
43876 ワード
みんな.
として、私はこのTweeter challengeをやっている
Db diagram
つぶやきを送信するフォームから始めましょう.
src/コンポーネント/tweets/tweetform.TSX
例えば、私はリンクを抽出し、それらを短縮する必要があります.私もフロントエンドでそれをする必要がなかったとしてもハッシュタグを抽出しました.
とにかく、addtweet関数に注目しましょう.
あなたが気がつくことができる最初は、私がアポロクライアントによって提供されたロードとエラーを使用していないということです.URLを短縮することができますので、関数が起動するとすぐに状態を読み込む必要があります.同じように、私はデータをyupライブラリで検証するのでエラーを処理する必要があります.
これは、ExtractMetadataとShortenurls関数がどのように見えるかです.
addRangeつぶやき変異について:
src/graphql/tweets/fragmentTS
リンクプレビューを表示するには、取得しなければなりません.そのためにdataloaderを使います.
src/DataLoader.TS
私もプレビューエンティティを追加
src/resolvers/tweetsresolvers.TS
src/events/scappreviewエミッタ.TS
プレビューコンポーネントを追加することでジョブを終了するフロントエンド側に戻りましょう.
src/コンポーネント/つぶやき/プレビュー.TSX
結果の小さなプレビューです.
私は、その部分について言いたかったことを多かれ少なかれ言いました.私が何か言及するのを忘れたならば、チェックしてください.そうでなければ、私に連絡し、コメントを残して自由に感じなさい.
さあ、お大事に.
として、私はこのTweeter challengeをやっている
Db diagram
リンクのプレビュー(フロントエンド)
つぶやきを送信するフォームから始めましょう.
src/コンポーネント/tweets/tweetform.TSX
import { ApolloError, useMutation } from '@apollo/client'
import { forwardRef, useEffect, useState } from 'react'
import { MdImage, MdPublic } from 'react-icons/md'
import { useRecoilValue, useSetRecoilState } from 'recoil'
import { ValidationError } from 'yup'
import { ADD_TWEET } from '../../graphql/tweets/mutations'
import { tweetsState } from '../../state/tweetsState'
import { userState } from '../../state/userState'
import { extractMetadata, handleErrors, shortenURLS } from '../../utils/utils'
import { addTweetSchema } from '../../validations/tweets/schema'
import Alert from '../Alert'
import Avatar from '../Avatar'
import Button from '../Button'
const TweetForm = () => {
// Global state
const user = useRecoilValue(userState)
const setTweets = useSetRecoilState(tweetsState)
// Local state
const [body, setBody] = useState('')
const [addTweetMutation, { data }] = useMutation(ADD_TWEET)
// I create a local state for loading instead of using the apollo loading
// because of the urlShortener function.
const [loading, setLoading] = useState(false)
const [errors, setErrors] = useState<ValidationError | null>(null)
const [serverErrors, setServerErrors] = useState<any[]>([])
const addTweet = async () => {
setErrors(null)
setServerErrors([])
setLoading(true)
// extract info from the tweet body ( urls, hashtags for now)
const { hashtags, urls } = await extractMetadata(body)
// Shorten the urls
let shortenedURLS: any
let newBody = body.slice() /* make a copy of the body */
if (urls && urls.length > 0) {
// Shorten the url via tinyURL
// Not ideal but ok for now as I didn't create my own service to shorten the url
// and I don't think I will create one ;)
shortenedURLS = await shortenURLS(urls)
shortenedURLS.forEach((el: any) => {
// Need to escape characters for the regex to work
const pattern = el.original.replace(/[^a-zA-Z0-9]/g, '\\$&')
newBody = newBody.replace(new RegExp(pattern), el.shorten)
})
}
try {
// I should not validate hashtags and shortenedURLS as
// it's an "intern" thing. I let it for now mostly for development purposes.
await addTweetSchema.validate({
body,
hashtags,
shortenedURLS,
})
await addTweetMutation({
variables: {
payload: {
body: newBody ?? body,
hashtags,
url: shortenedURLS ? shortenedURLS[0].shorten : null,
},
},
})
} catch (e) {
if (e instanceof ValidationError) {
setErrors(e)
} else if (e instanceof ApolloError) {
setServerErrors(handleErrors(e))
}
console.log('e', e)
} finally {
setLoading(false)
}
}
useEffect(() => {
if (data) {
setTweets((old) => {
return [data.addTweet].concat(old)
})
setBody('')
}
}, [data])
return (
<div className="mb-4 p-4 w-full rounded-lg shadow bg-white">
{serverErrors.length > 0 && (
<div className="mb-4">
{serverErrors.map((e: any, index: number) => {
return (
<Alert
key={index}
variant="danger"
message={Array.isArray(e) ? e[0].message : e.message}
/>
)
})}
</div>
)}
<h3>Tweet something</h3>
<hr className="my-2" />
<div className="flex w-full">
<Avatar className="mr-2" display_name={user!.display_name} />
<div className="w-full">
<div className="w-full mb-2">
<textarea
rows={5}
value={body}
onChange={(e) => setBody(e.target.value)}
className="w-full placeholder-gray4 p-2 "
placeholder="What's happening"
></textarea>
{errors && errors.path === 'body' && (
<span className="text-red-500 text-sm">{errors.message}</span>
)}
</div>
{/* Actions */}
<div className="flex justify-between">
<div className="flex items-center">
<MdImage className="text-primary mr-2" />
<div className="text-primary inline-flex items-center">
<MdPublic className="mr-1" />
<span className="text-xs">Everyone can reply</span>
</div>
</div>
<Button
text="Tweet"
variant="primary"
onClick={addTweet}
disabled={loading}
loading={loading}
/>
</div>
</div>
</div>
</div>
)
}
export default TweetForm
ここを見ることがたくさんあります.まず第一に、つぶやきは文字列ではありません.したがって、データを抽出するつもりです.これはすべてバックエンドで行うことができますが、私は何かを持っていないので、少なくとも今のところ、私は特定のイベント(RedisとPubsubの例)を聞くことができます、私はフロントエンド側で作業を行うことを決めた.例えば、私はリンクを抽出し、それらを短縮する必要があります.私もフロントエンドでそれをする必要がなかったとしてもハッシュタグを抽出しました.
とにかく、addtweet関数に注目しましょう.
あなたが気がつくことができる最初は、私がアポロクライアントによって提供されたロードとエラーを使用していないということです.URLを短縮することができますので、関数が起動するとすぐに状態を読み込む必要があります.同じように、私はデータをyupライブラリで検証するのでエラーを処理する必要があります.
これは、ExtractMetadataとShortenurls関数がどのように見えるかです.
export const extractMetadata = async (body: string) => {
let hashtags = body.match(/(#[\w]+)/g)
const urls = body.match(/https?:\/\/\S+/g)
// Remove duplicates
if (hashtags && hashtags?.length > 0) {
hashtags = Array.from(new Set(hashtags))
}
return {
hashtags,
urls,
}
}
export const shortenURLS = async (
urls: string[]
): Promise<{ original: string; shorten: string }[]> => {
const tinyURLS = []
for (let url of urls) {
const res = await TinyURL.shorten(url)
tinyURLS.push({
original: url,
shorten: res,
})
}
return tinyURLS
}
ここでの最大の問題は、URLを短くするために外部サービスを使うという事実です.それは少し時間がかかるので、フロントエンドでこれを行うことは理想から遠く離れている.しかしながら、私は特にURLを短くするために私自身のサービスをしたくありません.私はより良い解決方法は、バックグラウンドでURLの短縮を起動し、それが短縮URLを使用してつぶやきを更新するために行われれば、タスクを聞くためにRedisを使用することです.今のところ簡単にしましょう.addRangeつぶやき変異について:
export const ADD_TWEET = gql`
mutation($payload: AddTweetPayload!) {
addTweet(payload: $payload) {
...tweetFragment
}
}
${TWEET_FRAGMENT}
`
あなたが見ることができるように、私は自分自身を繰り返すことを好まないので、我々はGraphSQLの断片を使用することができます.断片はこちらsrc/graphql/tweets/fragmentTS
import { gql } from '@apollo/client'
export const TWEET_FRAGMENT = gql`
fragment tweetFragment on Tweet {
id
body
visibility
likesCount
retweetsCount
commentsCount
parent {
id
body
user {
id
username
display_name
avatar
}
}
preview {
id
title
description
url
image
}
isLiked
type
visibility
user {
id
username
display_name
avatar
}
created_at
}
`
私は“プレビュー”の部分を言ったとは思わない.バックエンドを通してクイックツアーをして、変更したものを見ましょうプレビューDataAlader (バックエンド)
リンクプレビューを表示するには、取得しなければなりません.そのためにdataloaderを使います.
src/DataLoader.TS
previewLinkDataloader: new DataLoader<number, unknown, unknown>(
async (ids) => {
const previews = await db('previews as p')
.innerJoin('previews_tweets as pt', 'pt.preview_id', '=', 'p.id')
.whereIn('pt.tweet_id', ids)
.select(['p.*', 'pt.tweet_id'])
return ids.map((id) => previews.find((p) => p.tweet_id === id))
}
),
今は慣れ始めています.私もプレビューエンティティを追加
import { Field, ObjectType } from 'type-graphql'
@ObjectType()
class Preview {
@Field()
id: number
@Field()
url: string
@Field()
title: string
@Field({ nullable: true })
description?: string
@Field({ nullable: true })
image?: string
}
export default Preview
と@ fieldresolver.src/resolvers/tweetsresolvers.TS
@FieldResolver(() => Preview)
async preview(@Root() tweet: Tweet, @Ctx() ctx: MyContext) {
const {
dataloaders: { previewLinkDataloader },
} = ctx
return await previewLinkDataloader.load(tweet.id)
}
また、いくつかの問題を回避するために、TweetResolverのaddTweet関数で、挿入されたつぶやきを返すときに別のものを追加しました.return {
...tweet,
likesCount: 0,
commentsCount: 0,
retweetsCount: 0,
}
最後に、リンクのプレビューを挿入した後、作成したDataCaladerのキャッシュをクリーンアップします.src/events/scappreviewエミッタ.TS
import { EventEmitter } from 'events'
import { scrap } from '../utils/utils'
import knex from '../db/connection'
import { dataloaders } from '../dataloaders/dataloaders'
const scrapPreviewEmitter = new EventEmitter()
scrapPreviewEmitter.on('scrap', async (url: string, tweet_id: number) => {
try {
const result = await scrap(url)
const previewsIds = await knex('previews')
.insert({
...result,
url,
})
.onConflict('url')
.ignore()
.returning('id')
const toInsert = previewsIds.map((id) => {
return {
preview_id: id,
tweet_id: tweet_id,
}
})
await knex('previews_tweets').insert(toInsert)
dataloaders.previewLinkDataloader.clear(tweet_id)
} catch (e) {
console.log('e', e)
}
})
export default scrapPreviewEmitter
ところで、前にしたことを少し変えました.そして、特に私が短くなったURLを挿入するという事実、私がスクラップで得たURLではない.さもなければ、フロントエンドにはマッチがないので、プレビューを表示できませんでした.プレビューコンポーネント
プレビューコンポーネントを追加することでジョブを終了するフロントエンド側に戻りましょう.
src/コンポーネント/つぶやき/プレビュー.TSX
const Preview = ({ preview }: any) => {
return (
<a
href={preview.url}
className="rounded shadow block p-3 hover:bg-gray3 transition-colors duration-300"
>
{preview.image && (
<img
className="rounded object-cover w-full"
src={preview.image}
alt={preview.title}
/>
)}
<h4 className="font-semibold my-2">{preview.title}</h4>
{preview.description && <p>{preview.description}</p>}
</a>
)
}
export default Preview
ここではとても複雑なものはない.それにもかかわらず、私はイメージのlazyloadに注意を払わなければならない.私はgithubに問題を加えました、それで、私は忘れません;.結果の小さなプレビューです.
私は、その部分について言いたかったことを多かれ少なかれ言いました.私が何か言及するのを忘れたならば、チェックしてください.そうでなければ、私に連絡し、コメントを残して自由に感じなさい.
さあ、お大事に.
Reference
この問題について([パート19 ][フロントエンド] GraphSQL、TypeScript、および反応を使用してTwitterのクローンを作成する), 我々は、より多くの情報をここで見つけました https://dev.to/ipscodingchallenge/part-19-frontend-creating-a-twitter-clone-with-graphql-typescript-and-react-link-s-preview-post-tweet-3kn3テキストは自由に共有またはコピーできます。ただし、このドキュメントのURLは参考URLとして残しておいてください。
Collection and Share based on the CC Protocol