برنامه نویسی

Crypto News Aggregator با استفاده از Typescript، Next.js، NewsDataHub و APIهای CoinGecko

Summarize this content to 400 words in Persian Lang
در این مقاله، می‌خواهیم یک برنامه ساده اما مفید برای جمع‌آوری اخبار ارزهای دیجیتال بسازیم که از NewsDataHub و APIهای CoinGecko استفاده می‌کند. هدف این مقاله یک توسعه‌دهنده سطح مبتدی است – اگر فکر می‌کنید هر بخش ارزشی به تجربه یادگیری شما نمی‌افزاید، می‌توانید از آن صرفنظر کنید.

همچنین می توانید ببینید که کد نهایی پروژه چگونه است: https://github.com/newsdatahub/crypto-news-aggregator

می توانید ببینید که نسخه تولیدی این برنامه دقیقاً در اینجا چگونه است: https://newsdatahub.com/crypto

بیایید با ایجاد یک پروژه Next.js جدید با پشتیبانی Typescript شروع کنیم.

npx create-next-app@latest crypto-news-aggregator –typescript

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

وقتی از شما خواسته شد، انتخاب کنید:

برای ESLint بله
نه برای Tailwind CSS (ما از ماژول‌های CSS استفاده خواهیم کرد)
نه برای src/ دایرکتوری
برای App Router بله
نه برای توربوپک
نه برای سفارشی کردن نام مستعار واردات (ما این را به صورت دستی تنظیم می کنیم)

cd در پوشه پروژه:

cd crypto-news-aggregator

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

راه اندازی ساختار پروژه

پس از مقداردهی اولیه، بیایید ساختار پروژه خود را ایجاد کنیم. من هدف هر دایرکتوری و فایل را توضیح خواهم داد.

mkdir -p app/components/{news-feed,price-ticker} __tests__ types

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

در پایان این آموزش باید به ساختار زیر برسید.

crypto-news-aggregator/
├── __tests__/ # Test files
│ ├── Home.test.tsx
│ ├── NewsCard.test.tsx
│ └── PriceTicker.test.tsx
├── app/ # Next.js app directory
│ ├── components/ # React components
│ │ ├── news-feed/ # News-related components
│ │ │ ├── NewsCard.tsx
│ │ │ └── index.ts
│ │ └── price-ticker/ # Price ticker components
│ │ ├── PriceTicker.tsx
│ │ └── index.ts
│ ├── layout.tsx # Root layout component
│ ├── page.module.css # Styles for main page
│ └── page.tsx # Main page component
├── public/ # Static assets
├── types/ # TypeScript type definitions
│ ├── cache.ts
│ ├── crypto.ts
│ ├── env.d.ts
│ ├── index.ts
│ └── news.ts
├── .env.example # Example environment variables
├── .env.local # Environment variables (gitignored)
├── .eslintrc.json # ESLint configuration
├── .gitignore # Git ignore rules
├── eslint.config.mjs # ESLint module configuration
├── jest.config.mjs # Jest configuration
├── jest.setup.js # Jest setup file
├── next-env.d.ts # Next.js TypeScript declarations
├── next.config.js # Next.js configuration
├── package-lock.json # Locked dependency versions
├── package.json # Project dependencies
├── README.md
├── tsconfig.json # TypeScript configuration
└── types.d.ts # Global TypeScript declarations

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

اما قبل از اینکه به آنجا برسیم، باید دایرکتوری پروژه را کمی تمیز کنیم و سپس چند فایل ایجاد کنیم.

فایل هایی که می توان با خیال راحت پاک کرد

app/globals.css (اگر از فایل های module.css استفاده می کنید)
همه .svg فایل ها (در /public دایرکتوری)
README.md (حذف یا به‌روزرسانی، زیرا این برنامه پیش‌فرض از create-next-app است)

اگر favicon.ico شما در دایرکتوری برنامه است، آن را به پوشه عمومی منتقل کنید. در حالی که فاویکون می تواند در هر دو مکان کار کند، آن را به آن منتقل کنید public/ از ساختار متعارف پیروی می کند و مکان دارایی را واضح تر می کند.

تست کردن

ما باید چندین بسته آزمایشی را نصب کنیم

npm install –save-dev @testing-library/react @testing-library/jest-dom jest jest-environment-jsdom

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

بیایید بفهمیم که هر بسته چه کاری انجام می دهد:

@testing-library/react: ابزارهایی را برای آزمایش اجزای React ارائه می دهد

@testing-library/jest-dom: همسان Jest سفارشی را اضافه می کند

jest: چارچوب اصلی تست

jest-environment-jsdom: یک محیط مرورگر را برای تست های ما شبیه سازی می کند

ایجاد کنید types.d.ts برای تست تعاریف نوع

import ‘@testing-library/jest-dom’;
declare global {
namespace jest {
interface Matchers<R> {
toBeInTheDocument(): R;
}
}
interface Window {
fetch: jest.Mock;
}
}
export {};

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

حال بیایید تعاریف نوع TypeScript را نصب کنیم تا ویرایشگر کد ما بتواند Node.js، React و Jest APIها را درک کند، و امکان تکمیل خودکار و گرفتن خطاهای نوع در طول توسعه را فراهم کند.

npm install –save-dev @types/node @types/react @types/jest

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

پس از نصب بسته ها، باید جست را پیکربندی کنیم. ایجاد یک jest.config.mjs فایل در ریشه پروژه خود

jest.config.mjs:

import nextJest from ‘next/jest.js’;

const createJestConfig = nextJest({
dir: ‘./’,
});

export default createJestConfig({
testEnvironment: ‘jest-environment-jsdom’,
setupFilesAfterEnv: [‘/jest.setup.js’] });

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

ایجاد یک jest.setup.js فایل برای وارد کردن تطبیق دهنده های DOM.

jest.setup.js:

import ‘@testing-library/jest-dom’;

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

در نهایت، این خطوط را اضافه کنید تا اسکریپت آزمایشی شما اجرا شود package.json زیر scripts:

“test”: “jest”,
“test:watch”: “jest –watch”

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

بنابراین به این صورت خواهد بود:

“scripts”: {
“dev”: “next dev”,
“build”: “next build”,
“start”: “next start”,
“lint”: “next lint”,
“test”: “jest”,
“test:watch”: “jest –watch”
},

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

اکنون می توانید با استفاده از تست ها را اجرا کنید npm test یا npm run test:watch برای حالت تماشا اما ما هنوز هیچ تستی نداریم، به زودی یک تست اضافه خواهیم کرد.

دریافت نشانه API NewsDataHub شما

بیایید روند دریافت توکن API شما را طی کنیم.

به NewsDataHub.com مراجعه کنید

ایجاد حساب کاربری (بدون نیاز به کارت اعتباری)

آدرس ایمیل خود را در فرم ثبت نام وارد کنید
باید ایمیل خود را برای کد تایید بررسی کنید
هنگامی که حساب خود را تأیید کردید، به داشبورد خود منتقل خواهید شد، جایی که می توانید کلید API خود را پیدا کنید

افزودن کلید API به پروژه شما

ایجاد یک .env.example در ریشه پروژه خود فایل کنید تا به عنوان یک الگو برای متغیرهای محیط مورد نیاز عمل کند

NEXT_PUBLIC_API_URL=https://api.newsdatahub.com/v1/news
NEXT_PUBLIC_API_TOKEN=your_token_here

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

سپس برای کپی موارد زیر را اجرا کنید .env.example قالب به .env.local که در آن پیکربندی واقعی شما خواهد بود

cp .env.example .env.local

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

جایگزین کنید your_token_here در .env.local با توکن NewsDataHub API خود از داشبورد خود.

.env.example به git به عنوان یک الگو متعهد است، while .env.local حاوی اسرار واقعی است و نادیده گرفته می شود.

هنگام ارجاع به کلید API از کلمات “token” و “key” به جای یکدیگر استفاده می کنیم. هنگامی که توکن API خود را دارید، آن را به پروژه خود اضافه کنید .env.local فایل:

NEXT_PUBLIC_API_URL=https://api.newsdatahub.com/v1
NEXT_PUBLIC_API_TOKEN=your_newsdatahub_token

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

تنظیمات و نمای کلی فایل های پیکربندی

بیایید فایل های پیکربندی ضروری را تنظیم کنیم. من هدف و محتوای هر یک را توضیح می دهم:

به روز رسانی .gitignore

این .gitignore فایل به طور خودکار در طول اولیه سازی پروژه ایجاد شد. به Git می گوید کدام فایل ها و پوشه ها را از کنترل نسخه حذف کند.

بیایید از خودمان مطمئن شویم .env.local فایل با افزودن موارد زیر به آن نادیده گرفته می شود .gitignore.

# Environment files
.env*.local

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

تنظیم تعاریف نوع محیط

ایجاد کنید types/env.d.ts برای ارائه تعاریف نوع TypeScript برای متغیرهای محیطی:

declare global {
namespace NodeJS {
interface ProcessEnv {
NEXT_PUBLIC_API_URL: string;
NEXT_PUBLIC_API_TOKEN: string;
}
}
}

export {};

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

این فایل به TypeScript در مورد متغیرهای محیطی ما می گوید و امکان بررسی صحیح نوع را در هنگام دسترسی فراهم می کند process.env مقادیر و ارائه پیشنهادات تکمیل خودکار. بدون آن، TypeScript این متغیرها را از نوع خود در نظر می گیرد any .

راه اندازی ESLint

اضافه کنید .eslintrc.json برای فعال کردن قوانین پیش‌فرض Next.js برای عملکرد و بهترین شیوه‌ها.

{
“extends”: [
“next/core-web-vitals”
] }

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

حالا بیایید کد پروژه را اضافه کنیم

ایجاد کنید types/cache.ts.ساختار سیستم حافظه پنهان سمت سرویس گیرنده ما را تعریف می کند و نحوه ذخیره مهرهای زمانی و داده های خبری را مشخص می کند.

import { NewsItem } from “.”;

export interface CacheData {
timestamp: number;
data: NewsItem[];
}

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

ایجاد کنید types/crypto.ts.ساختار داده قیمت ارزهای دیجیتال را از طریق CoinCap API، از جمله قیمت، ارزش بازار و تغییرات 24 ساعته تعریف می‌کند.

export type CoinData = {
[key: string]: {
usd: number;
usd_market_cap: number;
usd_24h_vol: number;
usd_24h_change: number;
last_updated_at: number;
}
}

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

ایجاد کنید news.tsحاوی رابط‌هایی برای موارد خبری از NewsDataHub API و لوازم جانبی برای مؤلفه NewsCard ما.

export interface NewsItem {
id: string;
title: string;
article_link: string;
description: string;
pub_date: string;
}

export interface NewsCardProps {
index: number;
item: NewsItem;
}

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

ایجاد کنید types/index.ts.نقطه صادرات مرکزی برای همه تعاریف نوع، امکان واردات پاک را فراهم می کند.

export * from ‘./cache’;
export * from ‘./news’;
export * from ‘./crypto’;

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

پیاده سازی کامپوننت NewsCard و PriceTicker

در مرحله بعد، ما اجزای خود و سبک آنها را پیاده سازی می کنیم. هر جزء باید در دایرکتوری مربوطه خود باشد.

جزء NewsCard

app/components/news-feed/NewsCard.tsx

import styles from ‘./styles.module.css’;
import { NewsCardProps } from ‘@/types’;

export const NewsCard: React.FC<NewsCardProps> = ({index, item}) => {
return (
<div key={index} className={styles.newsCard}>
<h2 className={styles.newsTitle}>{item.title}h2>
<p>{item.description.slice(0, 200)+”…”} Read more
<br/>
<br/>
<a href={item.article_link} target=”_blank” rel=”noopener noreferrer” className={styles.newsLink}>
{item.article_link}
a>
p>

<div className={styles.newsDate}>
{new Date(item.pub_date).toLocaleDateString()}
div>
div>
)
}

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

app/components/news-feed/styles.module.css

.newsCard {
padding: 15px;
border: 1px solid #ddd;
border-radius: 4px;
margin-bottom: 15px;
}

.newsTitle {
margin: 0 0 10px 0;
font-size: 1.2em;
color: #2e009a;
font-family: math;
}

.newsDate {
color: #666;
font-size: 0.9em;
margin-top: 10px;
}

.newsLink {
color:darkcyan;
}

.newsLink:hover {
color: rgb(3, 79, 79);
cursor: pointer;
text-decoration: underline;
}

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

app/components/news-feed/index.tsx

export { NewsCard } from ‘./NewsCard’;

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

کامپوننت PriceTicker

app/components/price-ticker/PriceTicker.tsx

import { useState, useEffect } from ‘react’;
import styles from ‘./styles.module.css’;
import { CoinData } from ‘@/types’;

export const PriceTicker = () => {
const [prices, setPrices] = useState<CoinData>({});
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(true);

const fetchPrices = async () => {
try {
const response = await fetch(‘https://api.coingecko.com/api/v3/simple/price?ids=bitcoin,ethereum,dogecoin&vs_currencies=usd&include_24hr_change=true’);
if (!response.ok) throw new Error(‘Failed to fetch prices’);

const data = await response.json();

setPrices(data);
setError(null);
} catch (err) {
setError(‘Failed to load prices’);
console.error(‘Price fetch error:’, err);
} finally {
setLoading(false);
}
};

useEffect(() => {
fetchPrices();
const interval = setInterval(fetchPrices, 60000); // Update every minute
return () => clearInterval(interval);
}, []);

if (loading) return <div className={styles.ticker}>Loading prices…div>;
if (error) return <div className={styles.ticker}>Price data unavailablediv>;

return (
<div className={styles.ticker}>
{Object.entries(prices).map(([coinId, data]) => {
const price = data.usd.toLocaleString(‘en-US’, {
style: ‘currency’,
currency: ‘USD’,
minimumFractionDigits: 2,
maximumFractionDigits: 2
});

const change = data.usd_24h_change || 0;
const changeClass = change >= 0 ? styles.positive : styles.negative;

return (
<div key={coinId} className={styles.cryptoPrice}>
<span className={styles.symbol}>{coinId.toUpperCase()}span>
<span className={styles.price}>{price}span>
<span className={`${styles.change} ${changeClass}`}>
{change >= 0 ? ‘↑’ : ‘↓’}
{Math.abs(change).toFixed(2)}%
span>
div>
);
})}
div>
);
}

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

app/components/price-ticker/styles.module.css

.ticker {
background: #1a1a1a;
color: white;
padding: 10px;
border-radius: 8px;
margin-bottom: 20px;
overflow-x: auto;
display: flex;
gap: 20px;
align-items: center;
}

.cryptoPrice {
display: flex;
align-items: center;
gap: 8px;
padding: 4px 8px;
background: rgba(255, 255, 255, 0.1);
border-radius: 4px;
white-space: nowrap;
}

.symbol {
font-weight: bold;
color: #ffd700;
}

.price {
font-family: monospace;
}

.change {
font-size: 0.9em;
padding: 2px 6px;
border-radius: 4px;
}

.positive {
color: #00ff00;
}

.negative {
color: #ff4444;
}

@keyframes slide {
0% {
transform: translateX(100%);
}
100% {
transform: translateX(-100%);
}
}

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

app/components/news-feed/index.tsx

export { PriceTicker } from ‘./PriceTicker’;

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

ساخت مؤلفه صفحه اصلی

در Next.js App Router، صفحه اصلی برنامه ما در آن قرار دارد app/page.tsx. در حالی که فایل قرار است نامگذاری شود page.tsx به دنبال قراردادهای Next.js، کامپوننت خود را نامگذاری می کنیم Home تا به وضوح هدف آن را به عنوان صفحه اصلی برنامه ما نشان دهد.

پیش بروید و به روز کنید app/components/page.tsx با کد زیر

‘use client’;

import { useState, useEffect } from ‘react’;
import { PriceTicker } from ‘@/app/components/price-ticker’;
import { NewsCard } from ‘@/app/components/news-feed’;
import { CacheData, NewsItem } from ‘@/types’;
import styles from ‘./page.module.css’;

// Environment variables for API configuration
const API_URL = process.env.NEXT_PUBLIC_API_URL;
const API_TOKEN = process.env.NEXT_PUBLIC_API_TOKEN;

// Cache duration set to one hour
const CACHE_DURATION = 1000 * 60 * 60;
const TOPICS = [‘cryptocurrency’];

// In-memory cache for storing news data
const cache: Record<string, CacheData> = {};

export default function Home() {
// State management using React hooks
const [news, setNews] = useState<NewsItem[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [lastUpdated, setLastUpdated] = useState<Date | null>(null);

// Fetches news data with built-in caching
const fetchNews = async (topics: string[]) => {
const cacheKey = topics.sort().join(‘,’);
const cachedData = cache[cacheKey];

if (cachedData && Date.now() – cachedData.timestamp < CACHE_DURATION) {
setNews(cachedData.data);
setLastUpdated(new Date(cachedData.timestamp));
return;
}

setLoading(true);
setError(null);

try {
const response = await fetch(`${API_URL}?language=en&topic=cryptocurrency`, {
headers: {
‘x-api-key’: API_TOKEN,
‘Content-Type’: ‘application/json’
},
});

if (!response.ok) throw new Error(‘Failed to fetch news’);

const articles = await response.json();
const data: NewsItem[] = articles.data;

cache[cacheKey] = {
timestamp: Date.now(),
data
};

setNews(data);
setLastUpdated(new Date());
} catch (err) {
setError(err instanceof Error ? err.message : ‘An error occurred’);
} finally {
setLoading(false);
}
};

// Fetch data when component mounts
useEffect(() => {
fetchNews(TOPICS);
}, []);

// Handler for manual refresh
const handleRefresh = () => {
const cacheKey = TOPICS.sort().join(‘,’);
delete cache[cacheKey];
fetchNews(TOPICS);
};

return (
<div className={styles.container}>
<PriceTicker />

{lastUpdated && (
<div className={styles.lastUpdated}>
Last updated: {lastUpdated.toLocaleTimeString()}
<button onClick={handleRefresh} className={styles.refreshButton}>
Refresh
button>
div>
)}

{error && <div className={styles.error}>{error}div>}

{loading ? (
<div className={styles.loading}>Loading…div>
) : (
news.map((item: NewsItem, index: number) => (
<NewsCard key={item.id || index} item={item} index={index} />
))
)}
div>
);
}

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

به روز رسانی page.module.css با سبک های زیر

.container {
max-width: 800px;
margin: 0 auto;
padding: 20px;
}

.topics {
margin-bottom: 20px;
}

.topic {
margin-right: 10px;
padding: 5px 10px;
border: 1px solid #ddd;
border-radius: 4px;
background: none;
cursor: pointer;
}

.topicSelected {
background: #007bff;
color: white;
border-color: #007bff;
}

.error {
color: #dc3545;
padding: 10px;
border: 1px solid #dc3545;
border-radius: 4px;
margin-bottom: 15px;
}

.loading {
text-align: center;
padding: 20px;
}

.lastUpdated {
color: #666;
font-size: 0.9em;
margin-bottom: 15px;
}

.refreshButton {
background: #007bff;
color: white;
border: none;
padding: 5px 10px;
border-radius: 4px;
cursor: pointer;
margin-left: 10px;
}

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

به روز رسانی app/layout.tsx با موارد زیر:

export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang=”en”>
<head>
<title>Crypto News Aggregator Applicationtitle>
<meta name=”description” content=”Crypto News Aggregator Application” />
head>
<body>
{children}
body>
html>
)
}

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

اجرای پروژه شما

ادامه دهید و پروژه خود را اجرا کنید

npm run dev

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

می توانید برنامه در حال اجرا خود را در https://localhost:3000 پیدا کنید

بابت اتمام پروژه تبریک می گویم! 🏆 👏

تست کامپوننت صفحه

ما می‌خواهیم آزمایشی برای مؤلفه Home اضافه کنیم که تأیید می‌کند که محتوای اخبار پس از واکشی داده‌ها به درستی ارائه می‌شود. ادامه دهید و این فایل آزمایشی را ایجاد کنید.

__tests__/Home.test.tsx

import { render, screen, waitFor } from ‘@testing-library/react’;
import Home from ‘@/app/page’;

describe(‘Home’, () => {
beforeEach(() => {
// Set up test environment variables
process.env.NEXT_PUBLIC_API_URL = ‘http://test-api.com’;
process.env.NEXT_PUBLIC_API_TOKEN = ‘test-token’;

// Mock fetch for both API endpoints
global.fetch = jest.fn((url) => {
// Mock responses for different API calls
if (url.includes(‘api.coingecko.com’)) {
return Promise.resolve({
ok: true,
json: () => Promise.resolve({
bitcoin: { usd: 65000, usd_24h_change: 2.5 }
}),
status: 200,
} as Response);
}
return Promise.resolve({
ok: true,
json: () => Promise.resolve({
data: [{
id: ‘1’,
title: ‘News Title’,
description: ‘News Description’,
url: ‘https://test.com’,
published_at: ‘2024-03-25’
}] }),
status: 200,
} as Response);
}) as jest.Mock;
});

test(‘renders news feed’, async () => {
render(<Home />);
await waitFor(
() => expect(screen.getByText(“News Title”)).toBeInTheDocument(),
{ timeout: 3000 }
);
});
});

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

تست را اجرا کنید

npm run test

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

این آزمایش تأیید می‌کند که مؤلفه Home ما با موفقیت محتوای اخبار را پس از واکشی داده‌ها ارائه می‌کند.

تست های اضافی برای اجزای PriceTicker و NewsCard را می توان در مخزن GitHub پروژه یافت. این تست‌ها عملکرد و رفتار رندرینگ را پوشش می‌دهند. من شما را تشویق می کنم که تست های بیشتری برای این پروژه ایجاد کنید.

بهبود بیشتر این پروژه را در نظر بگیرید.

چند ایده:

اجرای حالت های بارگذاری مناسب
صفحه بندی برای اخبار اضافه کنید
ذخیره سازی پیچیده تری را پیاده سازی کنید
مجموعه آزمایشی را تقویت کنید
می توانید پارامتر پرس و جو موضوع را برای واکشی انواع مختلف اخبار تغییر دهید

از اینکه دنبال می کنید متشکرم 😄

اعتبار تصویر جلد: عکس توسط پروژه سهام RDNE: https://www.pexels.com/photo/selective-focus-photo-of-silver-and-gold-bitcoins-8369648/

در این مقاله، می‌خواهیم یک برنامه ساده اما مفید برای جمع‌آوری اخبار ارزهای دیجیتال بسازیم که از NewsDataHub و APIهای CoinGecko استفاده می‌کند. هدف این مقاله یک توسعه‌دهنده سطح مبتدی است – اگر فکر می‌کنید هر بخش ارزشی به تجربه یادگیری شما نمی‌افزاید، می‌توانید از آن صرفنظر کنید.

همچنین می توانید ببینید که کد نهایی پروژه چگونه است: https://github.com/newsdatahub/crypto-news-aggregator

می توانید ببینید که نسخه تولیدی این برنامه دقیقاً در اینجا چگونه است: https://newsdatahub.com/crypto

بیایید با ایجاد یک پروژه Next.js جدید با پشتیبانی Typescript شروع کنیم.

npx create-next-app@latest crypto-news-aggregator --typescript
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

وقتی از شما خواسته شد، انتخاب کنید:

  • برای ESLint بله
  • نه برای Tailwind CSS (ما از ماژول‌های CSS استفاده خواهیم کرد)
  • نه برای src/ دایرکتوری
  • برای App Router بله
  • نه برای توربوپک
  • نه برای سفارشی کردن نام مستعار واردات (ما این را به صورت دستی تنظیم می کنیم)

cd در پوشه پروژه:

cd crypto-news-aggregator
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

راه اندازی ساختار پروژه

پس از مقداردهی اولیه، بیایید ساختار پروژه خود را ایجاد کنیم. من هدف هر دایرکتوری و فایل را توضیح خواهم داد.

mkdir -p app/components/{news-feed,price-ticker} __tests__ types
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

در پایان این آموزش باید به ساختار زیر برسید.

crypto-news-aggregator/
├── __tests__/                    # Test files
│   ├── Home.test.tsx
│   ├── NewsCard.test.tsx
│   └── PriceTicker.test.tsx
├── app/                          # Next.js app directory
│   ├── components/               # React components
│   │   ├── news-feed/            # News-related components
│   │   │   ├── NewsCard.tsx
│   │   │   └── index.ts
│   │   └── price-ticker/         # Price ticker components
│   │       ├── PriceTicker.tsx
│   │       └── index.ts
│   ├── layout.tsx               # Root layout component
│   ├── page.module.css          # Styles for main page
│   └── page.tsx                 # Main page component
├── public/                      # Static assets
├── types/                       # TypeScript type definitions
│   ├── cache.ts
│   ├── crypto.ts
│   ├── env.d.ts
│   ├── index.ts
│     └── news.ts
├── .env.example                # Example environment variables
├── .env.local                  # Environment variables (gitignored)
├── .eslintrc.json              # ESLint configuration
├── .gitignore                  # Git ignore rules
├── eslint.config.mjs           # ESLint module configuration
├── jest.config.mjs             # Jest configuration
├── jest.setup.js               # Jest setup file
├── next-env.d.ts               # Next.js TypeScript declarations
├── next.config.js              # Next.js configuration
├── package-lock.json           # Locked dependency versions
├── package.json                # Project dependencies
├── README.md
├── tsconfig.json               # TypeScript configuration
└── types.d.ts                  # Global TypeScript declarations
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

اما قبل از اینکه به آنجا برسیم، باید دایرکتوری پروژه را کمی تمیز کنیم و سپس چند فایل ایجاد کنیم.

فایل هایی که می توان با خیال راحت پاک کرد

  • app/globals.css (اگر از فایل های module.css استفاده می کنید)
  • همه .svg فایل ها (در /public دایرکتوری)
  • README.md (حذف یا به‌روزرسانی، زیرا این برنامه پیش‌فرض از create-next-app است)

اگر favicon.ico شما در دایرکتوری برنامه است، آن را به پوشه عمومی منتقل کنید. در حالی که فاویکون می تواند در هر دو مکان کار کند، آن را به آن منتقل کنید public/ از ساختار متعارف پیروی می کند و مکان دارایی را واضح تر می کند.

تست کردن

ما باید چندین بسته آزمایشی را نصب کنیم

npm install --save-dev @testing-library/react @testing-library/jest-dom jest jest-environment-jsdom
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

بیایید بفهمیم که هر بسته چه کاری انجام می دهد:

  • @testing-library/react: ابزارهایی را برای آزمایش اجزای React ارائه می دهد
  • @testing-library/jest-dom: همسان Jest سفارشی را اضافه می کند
  • jest: چارچوب اصلی تست
  • jest-environment-jsdom: یک محیط مرورگر را برای تست های ما شبیه سازی می کند

ایجاد کنید types.d.ts برای تست تعاریف نوع

import '@testing-library/jest-dom';
declare global {
  namespace jest {
    interface Matchers<R> {
      toBeInTheDocument(): R;
    }
  }
  interface Window {
    fetch: jest.Mock;
  }
}
export {};
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

حال بیایید تعاریف نوع TypeScript را نصب کنیم تا ویرایشگر کد ما بتواند Node.js، React و Jest APIها را درک کند، و امکان تکمیل خودکار و گرفتن خطاهای نوع در طول توسعه را فراهم کند.

npm install --save-dev @types/node @types/react @types/jest
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

پس از نصب بسته ها، باید جست را پیکربندی کنیم. ایجاد یک jest.config.mjs فایل در ریشه پروژه خود

jest.config.mjs:

import nextJest from 'next/jest.js';

const createJestConfig = nextJest({
  dir: './',
});

export default createJestConfig({
  testEnvironment: 'jest-environment-jsdom',
  setupFilesAfterEnv: ['/jest.setup.js']
});
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

ایجاد یک jest.setup.js فایل برای وارد کردن تطبیق دهنده های DOM.

jest.setup.js:

import '@testing-library/jest-dom';
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

در نهایت، این خطوط را اضافه کنید تا اسکریپت آزمایشی شما اجرا شود package.json زیر scripts:

"test": "jest",
"test:watch": "jest --watch"
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

بنابراین به این صورت خواهد بود:

  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint",
    "test": "jest",
    "test:watch": "jest --watch"
  },
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

اکنون می توانید با استفاده از تست ها را اجرا کنید npm test یا npm run test:watch برای حالت تماشا اما ما هنوز هیچ تستی نداریم، به زودی یک تست اضافه خواهیم کرد.

دریافت نشانه API NewsDataHub شما

بیایید روند دریافت توکن API شما را طی کنیم.

به NewsDataHub.com مراجعه کنید

ایجاد حساب کاربری (بدون نیاز به کارت اعتباری)

  • آدرس ایمیل خود را در فرم ثبت نام وارد کنید
  • باید ایمیل خود را برای کد تایید بررسی کنید
  • هنگامی که حساب خود را تأیید کردید، به داشبورد خود منتقل خواهید شد، جایی که می توانید کلید API خود را پیدا کنید

افزودن کلید API به پروژه شما

ایجاد یک .env.example در ریشه پروژه خود فایل کنید تا به عنوان یک الگو برای متغیرهای محیط مورد نیاز عمل کند

NEXT_PUBLIC_API_URL=https://api.newsdatahub.com/v1/news
NEXT_PUBLIC_API_TOKEN=your_token_here
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

سپس برای کپی موارد زیر را اجرا کنید .env.example قالب به .env.local که در آن پیکربندی واقعی شما خواهد بود

cp .env.example .env.local
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

جایگزین کنید your_token_here در .env.local با توکن NewsDataHub API خود از داشبورد خود.

.env.example به git به عنوان یک الگو متعهد است، while .env.local حاوی اسرار واقعی است و نادیده گرفته می شود.

هنگام ارجاع به کلید API از کلمات “token” و “key” به جای یکدیگر استفاده می کنیم. هنگامی که توکن API خود را دارید، آن را به پروژه خود اضافه کنید .env.local فایل:

NEXT_PUBLIC_API_URL=https://api.newsdatahub.com/v1
NEXT_PUBLIC_API_TOKEN=your_newsdatahub_token
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

تنظیمات و نمای کلی فایل های پیکربندی

بیایید فایل های پیکربندی ضروری را تنظیم کنیم. من هدف و محتوای هر یک را توضیح می دهم:

به روز رسانی .gitignore

این .gitignore فایل به طور خودکار در طول اولیه سازی پروژه ایجاد شد. به Git می گوید کدام فایل ها و پوشه ها را از کنترل نسخه حذف کند.

بیایید از خودمان مطمئن شویم .env.local فایل با افزودن موارد زیر به آن نادیده گرفته می شود .gitignore.

# Environment files
.env*.local
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

تنظیم تعاریف نوع محیط

ایجاد کنید types/env.d.ts برای ارائه تعاریف نوع TypeScript برای متغیرهای محیطی:

declare global {
  namespace NodeJS {
    interface ProcessEnv {
      NEXT_PUBLIC_API_URL: string;
      NEXT_PUBLIC_API_TOKEN: string;
    }
  }
}

export {};
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

این فایل به TypeScript در مورد متغیرهای محیطی ما می گوید و امکان بررسی صحیح نوع را در هنگام دسترسی فراهم می کند process.env مقادیر و ارائه پیشنهادات تکمیل خودکار. بدون آن، TypeScript این متغیرها را از نوع خود در نظر می گیرد any .

راه اندازی ESLint

اضافه کنید .eslintrc.json برای فعال کردن قوانین پیش‌فرض Next.js برای عملکرد و بهترین شیوه‌ها.

{
  "extends": [
    "next/core-web-vitals"
  ]
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

حالا بیایید کد پروژه را اضافه کنیم

ایجاد کنید types/cache.ts.

ساختار سیستم حافظه پنهان سمت سرویس گیرنده ما را تعریف می کند و نحوه ذخیره مهرهای زمانی و داده های خبری را مشخص می کند.

import { NewsItem } from ".";

export interface CacheData {
    timestamp: number;
    data: NewsItem[];
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

ایجاد کنید types/crypto.ts.
ساختار داده قیمت ارزهای دیجیتال را از طریق CoinCap API، از جمله قیمت، ارزش بازار و تغییرات 24 ساعته تعریف می‌کند.

export type CoinData = {
  [key: string]: {
    usd: number;
    usd_market_cap: number;
    usd_24h_vol: number;
    usd_24h_change: number;
    last_updated_at: number;
  }
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

ایجاد کنید news.ts
حاوی رابط‌هایی برای موارد خبری از NewsDataHub API و لوازم جانبی برای مؤلفه NewsCard ما.

export interface NewsItem {
    id: string;
    title: string;
    article_link: string;
    description: string;
    pub_date: string;
  }

export interface NewsCardProps {
    index: number;
    item: NewsItem;
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

ایجاد کنید types/index.ts.
نقطه صادرات مرکزی برای همه تعاریف نوع، امکان واردات پاک را فراهم می کند.

export * from './cache';
export * from './news';
export * from './crypto';
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

پیاده سازی کامپوننت NewsCard و PriceTicker

در مرحله بعد، ما اجزای خود و سبک آنها را پیاده سازی می کنیم. هر جزء باید در دایرکتوری مربوطه خود باشد.

جزء NewsCard

app/components/news-feed/NewsCard.tsx

import styles from './styles.module.css';
import { NewsCardProps } from '@/types';

export const NewsCard: React.FC<NewsCardProps> = ({index, item}) => {
    return (
        <div key={index} className={styles.newsCard}>
        <h2 className={styles.newsTitle}>{item.title}h2>
        <p>{item.description.slice(0, 200)+"..."} Read more 
        <br/>
        <br/>
            <a href={item.article_link} target="_blank"  rel="noopener noreferrer" className={styles.newsLink}>
            {item.article_link}
            a>    
        p>

        <div className={styles.newsDate}>
          {new Date(item.pub_date).toLocaleDateString()}
        div>
      div>
    )
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

app/components/news-feed/styles.module.css

.newsCard {
  padding: 15px;
  border: 1px solid #ddd;
  border-radius: 4px;
  margin-bottom: 15px;
}

.newsTitle {
  margin: 0 0 10px 0;
  font-size: 1.2em;
  color: #2e009a;
  font-family: math;
}

.newsDate {
  color: #666;
  font-size: 0.9em;
  margin-top: 10px;
}

.newsLink {
  color:darkcyan;
}

.newsLink:hover {
  color: rgb(3, 79, 79);
  cursor: pointer;
  text-decoration: underline;
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

app/components/news-feed/index.tsx

export { NewsCard } from './NewsCard';
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

کامپوننت PriceTicker

app/components/price-ticker/PriceTicker.tsx

import { useState, useEffect } from 'react';
import styles from './styles.module.css';
import { CoinData } from '@/types';

export const  PriceTicker = () => {
  const [prices, setPrices] = useState<CoinData>({});
  const [error, setError] = useState<string | null>(null);
  const [loading, setLoading] = useState(true);

  const fetchPrices = async () => {
    try {
      const response = await fetch('https://api.coingecko.com/api/v3/simple/price?ids=bitcoin,ethereum,dogecoin&vs_currencies=usd&include_24hr_change=true');
      if (!response.ok) throw new Error('Failed to fetch prices');

      const data = await response.json();

      setPrices(data);
      setError(null);
    } catch (err) {
      setError('Failed to load prices');
      console.error('Price fetch error:', err);
    } finally {
      setLoading(false);
    }
  };

  useEffect(() => {
    fetchPrices();
    const interval = setInterval(fetchPrices, 60000); // Update every minute
    return () => clearInterval(interval);
  }, []);

  if (loading) return <div className={styles.ticker}>Loading prices...div>;
  if (error) return <div className={styles.ticker}>Price data unavailablediv>;

  return (
    <div className={styles.ticker}>
      {Object.entries(prices).map(([coinId, data]) => {
        const price = data.usd.toLocaleString('en-US', {
          style: 'currency',
          currency: 'USD',
          minimumFractionDigits: 2,
          maximumFractionDigits: 2
        });

        const change = data.usd_24h_change || 0;
        const changeClass = change >= 0 ? styles.positive : styles.negative;

        return (
            <div key={coinId} className={styles.cryptoPrice}>
              <span className={styles.symbol}>{coinId.toUpperCase()}span>
              <span className={styles.price}>{price}span>
              <span className={`${styles.change} ${changeClass}`}>
       {change >= 0 ? '' : ''}
                {Math.abs(change).toFixed(2)}%
     span>
            div>
        );
      })}
    div>
);
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

app/components/price-ticker/styles.module.css

.ticker {
  background: #1a1a1a;
  color: white;
  padding: 10px;
  border-radius: 8px;
  margin-bottom: 20px;
  overflow-x: auto;
  display: flex;
  gap: 20px;
  align-items: center;
}

.cryptoPrice {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 4px 8px;
  background: rgba(255, 255, 255, 0.1);
  border-radius: 4px;
  white-space: nowrap;
}

.symbol {
  font-weight: bold;
  color: #ffd700;
}

.price {
  font-family: monospace;
}

.change {
  font-size: 0.9em;
  padding: 2px 6px;
  border-radius: 4px;
}

.positive {
  color: #00ff00;
}

.negative {
  color: #ff4444;
}

@keyframes slide {
  0% {
    transform: translateX(100%);
  }
  100% {
    transform: translateX(-100%);
  }
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

app/components/news-feed/index.tsx

export { PriceTicker } from './PriceTicker';
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

ساخت مؤلفه صفحه اصلی

در Next.js App Router، صفحه اصلی برنامه ما در آن قرار دارد app/page.tsx. در حالی که فایل قرار است نامگذاری شود page.tsx به دنبال قراردادهای Next.js، کامپوننت خود را نامگذاری می کنیم Home تا به وضوح هدف آن را به عنوان صفحه اصلی برنامه ما نشان دهد.

پیش بروید و به روز کنید app/components/page.tsx با کد زیر

'use client';

import { useState, useEffect } from 'react';
import { PriceTicker } from '@/app/components/price-ticker';
import { NewsCard } from '@/app/components/news-feed';
import { CacheData, NewsItem } from '@/types';
import styles from './page.module.css';

// Environment variables for API configuration
const API_URL = process.env.NEXT_PUBLIC_API_URL;
const API_TOKEN = process.env.NEXT_PUBLIC_API_TOKEN;

// Cache duration set to one hour
const CACHE_DURATION = 1000 * 60 * 60;
const TOPICS = ['cryptocurrency'];

// In-memory cache for storing news data
const cache: Record<string, CacheData> = {};

export default function Home() {
  // State management using React hooks
  const [news, setNews] = useState<NewsItem[]>([]);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);
  const [lastUpdated, setLastUpdated] = useState<Date | null>(null);

  // Fetches news data with built-in caching
  const fetchNews = async (topics: string[]) => {
    const cacheKey = topics.sort().join(',');
    const cachedData = cache[cacheKey];

    if (cachedData && Date.now() - cachedData.timestamp < CACHE_DURATION) {
      setNews(cachedData.data);
      setLastUpdated(new Date(cachedData.timestamp));
      return;
    }

    setLoading(true);
    setError(null);

    try {
      const response = await fetch(`${API_URL}?language=en&topic=cryptocurrency`, {
        headers: {
          'x-api-key': API_TOKEN,
          'Content-Type': 'application/json'
        },
      });

      if (!response.ok) throw new Error('Failed to fetch news');

      const articles = await response.json();
      const data: NewsItem[] = articles.data;

      cache[cacheKey] = {
        timestamp: Date.now(),
        data
      };

      setNews(data);
      setLastUpdated(new Date());
    } catch (err) {
      setError(err instanceof Error ? err.message : 'An error occurred');
    } finally {
      setLoading(false);
    }
  };

  // Fetch data when component mounts
  useEffect(() => {
    fetchNews(TOPICS);
  }, []);

  // Handler for manual refresh
  const handleRefresh = () => {
    const cacheKey = TOPICS.sort().join(',');
    delete cache[cacheKey];
    fetchNews(TOPICS);
  };

  return (
    <div className={styles.container}>
      <PriceTicker />

      {lastUpdated && (
        <div className={styles.lastUpdated}>
          Last updated: {lastUpdated.toLocaleTimeString()}
          <button onClick={handleRefresh} className={styles.refreshButton}>
            Refresh
          button>
        div>
      )}

      {error && <div className={styles.error}>{error}div>}

      {loading ? (
        <div className={styles.loading}>Loading...div>
      ) : (
        news.map((item: NewsItem, index: number) => (
          <NewsCard key={item.id || index} item={item} index={index} />
        ))
      )}
    div>
  );
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

به روز رسانی page.module.css با سبک های زیر

.container {
  max-width: 800px;
  margin: 0 auto;
  padding: 20px;
}

.topics {
  margin-bottom: 20px;
}

.topic {
  margin-right: 10px;
  padding: 5px 10px;
  border: 1px solid #ddd;
  border-radius: 4px;
  background: none;
  cursor: pointer;
}

.topicSelected {
  background: #007bff;
  color: white;
  border-color: #007bff;
}

.error {
  color: #dc3545;
  padding: 10px;
  border: 1px solid #dc3545;
  border-radius: 4px;
  margin-bottom: 15px;
}

.loading {
  text-align: center;
  padding: 20px;
}

.lastUpdated {
  color: #666;
  font-size: 0.9em;
  margin-bottom: 15px;
}

.refreshButton {
  background: #007bff;
  color: white;
  border: none;
  padding: 5px 10px;
  border-radius: 4px;
  cursor: pointer;
  margin-left: 10px;
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

به روز رسانی app/layout.tsx با موارد زیر:

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <head>
        <title>Crypto News Aggregator Applicationtitle>
        <meta name="description" content="Crypto News Aggregator Application" />
      head>
      <body>
        {children}
      body>
    html>
  )
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

اجرای پروژه شما

ادامه دهید و پروژه خود را اجرا کنید

npm run dev
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

می توانید برنامه در حال اجرا خود را در https://localhost:3000 پیدا کنید

بابت اتمام پروژه تبریک می گویم! 🏆 👏

تست کامپوننت صفحه

ما می‌خواهیم آزمایشی برای مؤلفه Home اضافه کنیم که تأیید می‌کند که محتوای اخبار پس از واکشی داده‌ها به درستی ارائه می‌شود. ادامه دهید و این فایل آزمایشی را ایجاد کنید.

__tests__/Home.test.tsx


import { render, screen, waitFor } from '@testing-library/react';
import Home from '@/app/page';

describe('Home', () => {
  beforeEach(() => {
    // Set up test environment variables
    process.env.NEXT_PUBLIC_API_URL = 'http://test-api.com';
    process.env.NEXT_PUBLIC_API_TOKEN = 'test-token';

    // Mock fetch for both API endpoints
    global.fetch = jest.fn((url) => {
      // Mock responses for different API calls
      if (url.includes('api.coingecko.com')) {
        return Promise.resolve({
          ok: true,
          json: () => Promise.resolve({
            bitcoin: { usd: 65000, usd_24h_change: 2.5 }
          }),
          status: 200,
        } as Response);
      }
      return Promise.resolve({
        ok: true,
        json: () => Promise.resolve({
          data: [{
            id: '1',
            title: 'News Title',
            description: 'News Description',
            url: 'https://test.com',
            published_at: '2024-03-25'
          }]
        }),
        status: 200,
      } as Response);
    }) as jest.Mock;
  });

  test('renders news feed', async () => {
    render(<Home />);
    await waitFor(
      () => expect(screen.getByText("News Title")).toBeInTheDocument(),
      { timeout: 3000 }
    );
  });
});
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

تست را اجرا کنید

npm run test
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

این آزمایش تأیید می‌کند که مؤلفه Home ما با موفقیت محتوای اخبار را پس از واکشی داده‌ها ارائه می‌کند.

تست های اضافی برای اجزای PriceTicker و NewsCard را می توان در مخزن GitHub پروژه یافت. این تست‌ها عملکرد و رفتار رندرینگ را پوشش می‌دهند. من شما را تشویق می کنم که تست های بیشتری برای این پروژه ایجاد کنید.

بهبود بیشتر این پروژه را در نظر بگیرید.

چند ایده:

  • اجرای حالت های بارگذاری مناسب
  • صفحه بندی برای اخبار اضافه کنید
  • ذخیره سازی پیچیده تری را پیاده سازی کنید
  • مجموعه آزمایشی را تقویت کنید
  • می توانید پارامتر پرس و جو موضوع را برای واکشی انواع مختلف اخبار تغییر دهید

از اینکه دنبال می کنید متشکرم 😄

اعتبار تصویر جلد: عکس توسط پروژه سهام RDNE: https://www.pexels.com/photo/selective-focus-photo-of-silver-and-gold-bitcoins-8369648/

نوشته های مشابه

دیدگاهتان را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *

دکمه بازگشت به بالا