با Sanity، Node Server و React یک رابط کاربری کاملا پویا بسازید

Summarize this content to 400 words in Persian Lang
ساختن با لگو همیشه مورد علاقه من بوده است. چیدن یک پازل سه بعدی، چه از روی محدودیت های یک کتابچه راهنمای کاربر یا در حوزه خلاق، ترکیبی منحصر به فرد از تحریک ذهنی و آزمونی از صبر را ارائه می دهد.
این خلاقیت در ساختن ساختارهای مختلف بر اساس یک هسته یک ویژگی مفید در بسیاری از جنبه های زندگی است، چه رسد به توسعه وب. در طول این مقاله، ما بر اساس این مفهوم برای ایجاد یک رابط کاربری کاملا پویا خواهیم بود. مانند مونتاژ بلوکهای لگو، ما بخشهایی از محتوا را گرد هم میآوریم، جایی که شما کنترل کامل نتیجه نهایی را در دست دارید.
مفهوم
بنابراین ایده این است که یک درخواست اولیه برای بازیابی مسیرهای برنامه از Sanity انجام دهیم و به صورت پویا یک روتر React را با آنها پیکربندی کنیم. این مسیرها همراه با نام مسیر حاوی ارجاع به صفحه پویا مورد نظر هستند و برای بازیابی صفحه به همراه بخش های مرتبط با آن استفاده می شود. محتوای ایستا مانند هدر و پاورقی فراتر از خروجی وجود دارد تا بین انتقال مسیر ثابت بماند.
ساختن نمونه سلامتی ما
از آنجایی که تمام محتوای برنامه ما از Sanity ارائه می شود، این مکان منطقی برای شروع است. Sanity یک راه حل محبوب برای ارائه محتوا به برنامه های مصرف کننده به شیوه ای انعطاف پذیر و پویا است. می توانید دستورالعمل های موجود در این صفحه را در مورد نحوه تنظیم آن دنبال کنید.
پس از تکمیل تنظیمات اولیه، ساختار پوشه زیر را در برنامه Sanity اضافه کنید.
|– …
|– schemas
|– documents
|- application.ts
|– dynamicPage
|– sections
|- SectionOne.ts
|- SectionTwo.ts
|– …
|- index.ts
|- dynamicPage.ts
|– objects
|- dynamicRoute.ts
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
بخش ها همانطور که در تصویر بالا توضیح داده شده است، یک بخش مجزا از محتوا را در صفحه تعریف می کند. این می تواند همه چیز از یک باشد HeroBanner در بالای صفحه به یک عنوان ساده، و مانند این خواهد بود.
// schemas/dynamicPage/sections/sectionOne.ts
export const sectionOne = defineField({
title: “Section One”,
name: “sectionOne”,
type: “object”,
fields: [
defineField({
title: “Properties”,
name: “properties”,
type: “object”,
fields: [
defineField({
title: “Heading”,
name: “heading”,
type: “string”,
description: “The heading for the banner.”,
}),
]
})
]
});
// schemas/dynamicPage/sections/sectionTwo.ts
export const sectionTwo = defineField({
title: “Section Two”,
name: “sectionTwo”,
type: “object”,
fields: [
defineField({
title: “Properties”,
name: “properties”,
type: “object”,
fields: [
defineField({
title: “Heading”,
name: “heading”,
type: “string”,
description: “The heading for the banner.”,
}),
]
})
]
});
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
این خواص نامگذاری برای کسانی که پسزمینه React دارند آشنا خواهد بود، زیرا ایده این است که این مقادیر را بهعنوان پایه به کامپوننت مورد نظر منتقل کنند.
سپس باید یک فیلد آرایه برای صادرات این بخش ها ایجاد کنیم. ما همچنین یک را اضافه می کنیم مرتب سازی با یک localCompare fn برای مرتب کردن آنها بر اساس حروف الفبا
// schemas/dynamicPage/sections/sections.ts
import {sectionOne} from “./sectionOne”
import {sectionTwo} from “./sectionTwo”
import {defineField} from “sanity”
const sectionsByName= [
sectionOne,
sectionTwo
].sort((a, b) => a.title!.localeCompare(b.title!))
export const sections = () =>
defineField({
title: “Sections”,
name: “sections”,
type: “array”,
of: sectionsByName,
})
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
یک صفحه پویا از یک یا چند بخش به هر ترتیب خاص تشکیل شده است و همچنین دارای عنوانی برای سهولت ارجاع است.
// schemas/dynamicPage/dynamicPage.ts
import {sections} from “./sections/sections”
export const dynamicPage = defineField({
title: “Dynamic Pages”,
name: “dynamicPage”,
type: “document”,
fields: [
defineField({
title: “Title”,
name: “title”,
type: “string”
}),
sections()
]
})
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
این مسیر پویا شامل تمام مسیرهای برنامه ما با ارجاع به صفحه(های) پویا خواهد بود و در سند برنامه زندگی می کند، که ساختار کل برنامه را مشخص می کند.
// objects/dynamicRoute
export const dynamicRoute = defineField({
title: “Dynamic Route”,
name: “dynamicRoute”,
type: “object”,
fields: [
defineField({
title: “Path”,
name: “path”,
type: “string”,
description:
“The path this route should match. Supports all route expressions that expressjs supports.”,
validation: (Rule) =>
Rule.required().custom((path: string | undefined) =>
(path || “”).startsWith(“https://dev.to/”) ? true : “Path must start with /”
),
}),
defineField({
title: “Properties”,
name: “properties”,
type: “object”,
fields: [
defineField({
title: “Reference page”,
name: “page”,
type: “reference”,
to: [{type: “dynamicPage”}],
})
]
}),
]
});
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
// schemas/documents/application.ts
export const application = defineField({
name: “application”,
type: “document”,
description: “Describes the central configuration for an application”,
fields: [
defineField({
title: “Application name”,
name: “name”,
type: “string”,
validation: (Rule) => Rule.required()
}),
defineField({
title: “Description”,
name: “description”,
type: “string”,
fieldset: “info”,
description: “A short description of this application.”,
}),
defineField({
title: “Routes”,
name: “routes”,
type: “array”,
of: [dynamicRoute],
options: {
layout: “list”,
},
})
]
});
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
در نهایت، ما باید مدارک خود را در بین آنها ثبت کنیم schemaTypes.
// schemaTypes/index.ts
import { application } from “../schemas/documents/application”;
import { dynamicPage } from “../schemas/dynamicPage/dynamicPage”;
export const schemaTypes = [application, dynamicPage]
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
اضافه کردن محتوا
راهاندازی و اجرا کردن نمونه CMS هم خوب و خوب است، اما هنوز باید مقداری محتوا اضافه کنیم.
با افزودن یک سند صفحه پویا با دو بخش شروع کنید – بخش یک و بخش دوم.
سپس باید یک اپلیکیشن ایجاد کنیم و مسیری را با ارجاع به صفحه پویا اضافه کنیم.
در نهایت تغییرات را منتشر کنید.
سرور نود با پرس و جوهای GROQ
اکنون که نمونه Sanity ما به پایان رسیده است، باید محتوا را مصرف کنیم. در اینجا، از یک Node Server با یک کلاینت Sanity برای انجام پرس و جوهای GROQ در مورد نمونه استفاده می کنیم. سپس محتوا را از طریق نقاط پایانی ایجاد شده با مشتری در معرض دید مشتری قرار می دهیم چارچوب اکسپرس.
با مقداردهی اولیه یک برنامه Node جدید شروع کنید و بسته های زیر را نصب کنید.
npm init -y
npm install express @sanity/client cors
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
از آنجایی که می خواهیم در اینجا نیز از TypeScript استفاده کنیم، باید از طریق دستور زیر آن را در داخل برنامه پیکربندی کنیم و انواع لازم را نصب کنیم.
npx tsc –init
npm i -D typescript ts-node @types/node @types/express @types/cors
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
به ساختن ساختار زیر در سرور ادامه دهید.
|– queries
| |- SanityDynamicRoutesQuery.ts
| |- SanityDynamicPageQuery.ts
|- start.ts
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
این start.ts فایل نقطه ورودی سرور خواهد بود و حاوی نقاط پایانی ما به همراه تنظیمات CORS برای مبدا مشتری است.
به منظور ایجاد پرس و جوهای Sanity قابل اجرا، به برخی تنظیمات اضافی نیاز داریم تا بتوانیم ارتباطی با نمونه خود برقرار کنیم. این با اضافه کردن تنظیمات مورد نظر در داخل به دست می آید createClient روش، و همه در Sanity موجود است.
// start.ts
import {createClient} from ‘@sanity/client’
import { SanityDynamicPageQuery } from ‘./queries/SanityDynamicPageQuery’
import { SanityDynamicRoutesQuery } from ‘./queries/SanityDynamicRoutesQuery’
import { Request, Response } from ‘express’
import cors from “cors”;
export const client = createClient({
projectId: ‘your-project-id’,
dataset: ‘your-dataset’,
useCdn: false, // set to `false` to bypass the edge cache
apiVersion: ‘2024-02-29’, // use current date (YYYY-MM-DD) to target the latest API version
})
var express = require(‘express’)
var app = express()
const allowedOrigins = [‘https://localhost:5173’]; // standard Vite port
const options: cors.CorsOptions = {
origin: allowedOrigins
};
app.use(cors(options));
app.get(‘/api/v1/dynamicRoutes’, async function (req: Request, res: Response) {
var dynamicRoutes = await SanityDynamicRoutesQuery();
return res.send(dynamicRoutes);
})
app.get(‘/api/v1/dynamicPage/:id’, async function (req: Request, res: Response) {
var dynamicPage = await SanityDynamicPageQuery(req.params.id)
return res.send(dynamicPage);
})
app.listen(3000, () => {
console.log(`[server]: Server is running at http://localhost:3000`);
})
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
شروع با SanityDynamicRouteQuery، برای بازیابی داده ها از Sanity باید یک پرس و جو GROQ انجام دهیم. شما می توانید با استفاده از افزونه Sanity Vision در نمونه CMS، این پرسش ها را آزمایش کنید.
درخواست زیر همه مسیرهای پویا منتشر شده برنامه مورد نظر را به استثنای اسناد پیش نویس برمی گرداند.
// queries/SanityDynamicRoutesQuery.ts
export const SanityDynamicRoutesQuery = async () => {
const groqQuery = `
*[!(_id in path(‘drafts.**’)) && _type == “application”][0]{routes}`;
const data = await client.fetch(groqQuery);
return data
}
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
سپس اضافه کنید SanityDynamicPageQuery فایل. این صفحه پویا را با استفاده از شناسه مرجع از خروجی در پرس و جو می کند SanityDynamicRoutesQuery، و بخش های لیست را با ویژگی های مرتبط آن برگردانید.
// queries/SanityDynamicPageQuery.ts
export const SanityDynamicPageQuery = async (id: string) => {
const groqQuery = `*[
_type == “dynamicPage”
&& (_id == ‘${id}’)]{
id{current},
sections{
_type,
properties,
},
…
}[0]`
const data = await client.fetch(groqQuery);
return data;
}
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
سرور را راه اندازی کنید و هر دوی این نقاط پایانی را با Postman یا ابزاری مشابه فراخوانی کنید تا مطمئن شوید که همه چیز طبق خواسته کار می کند.
npx ts-node start.ts
[server]: Server is running at http://localhost:3000وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
ساخت رابط کاربری پویا
با شروع نسخه پایدار اخیر Node، از دستور زیر برای مقداردهی اولیه برنامه خود استفاده می کنیم به سرعت، واکنش نشان دهید و TypesScript.
npm create vite@latest
> React
> TypeScript
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
پس از اتمام فرمان، با مقداردهی اولیه برنامه، بررسی کنید که همه چیز کار می کند.
npm run dev
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
این یک سرور توسعه دهنده محلی در http را راه اندازی می کند. برای مدیریت گواهیهای محلی با Vite، از vite-plugin-mkcert با گزینههای پیکربندی زیر استفاده کنید.
ما همچنین از این فرصت برای نصب استفاده می کنیم react-router-dom بسته بندی
npm i react-router-dom
npm i -D vite-plugin-mkcert
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
// vite.config.ts
import {defineConfig} from’vite’
import mkcert from ‘vite-plugin-mkcert’
export default defineConfig({
plugins: [react(), mkcert()]
server: {
https: true
},
})
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
برنامه bare bone اکنون باید پس از مقداردهی اولیه روی https راهاندازی و اجرا شود و میتوانیم ساختار زیر را اضافه کنیم.
|– features
|– DynamicPage
|- DynamicPageWrapper.tsx
|- DynamicPage.tsx
|- SectionErrorBoundary.tsx
|– Sections
|– SectionOne
|- SectionOne.tsx
|- SectionOne.module.scss
|- index.ts
|– SectionTwo
|- SectionTwo.tsx
|- SectionTwo.module.scss
|- index.ts
|– Layout
|- MainContainer.tsx
|- Footer.tsx
|- Header.tsx
|- NotFound.tsx
|- index.ts
|– routes
|- routesConfig.ts
|– interfaces
|- IDynamicSection.ts
|- App.tsx
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
با شروع از رابطها، باید چندین مورد از آنها را برای مدیریت محتوای پویا اضافه کنیم. این DynamicSectionFinder یک رابط برای یک تابع نگاشت است و در ادامه با جزئیات بیشتر توضیح داده خواهد شد. این IDynamicRoute رابطی است که ما برای مسیرهای پویا استفاده خواهیم کرد و شامل یک مسیر و خصوصیات است. این IDynamicSectionProps، بخش IDynamic و IDynamicSectionValues همه اینترفیسهایی هستند که به بخشهای یک صفحه پویا مربوط میشوند و با پیشروی واضحتر میشوند.
// interfaces/IDynamicSectionFinder
export type DynamicSectionFinder =
(name: string) => IDynamicSectionValues | undefined;
// interfaces/IDynamicRoute
export interface IDynamicRoute {
path: string;
properties: Properties;
}
// interfaces/IDynamicSection
export interface IDynamicSectionProps {
properties: T;
}
// interfaces/IDynamicSection
export interface IDynamicSection {
name: string;
displayComponent: React.ComponentType>;
}
// interfaces/IDynamicSectionValues
export interface IDynamicSectionValues {
_type: string;
properties: Properties;
}
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
حرکت به سمت routesConfig.ts فایل، محتوای زیر را داخل آن اضافه کنید.
import {NotFound} from “../features/NotFound/NotFound.tsx”
export const routesConfig = [
{
path: “*”,
element:
},
]
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
بله، در حال حاضر فقط یک مسیر، زیرا مسیرهای دیگر به صورت پویا اضافه خواهند شد.
سپس به عمیق ترین سطح ساختار پویا خود یعنی بخش ها می رویم. شروع با بخش یک- و دو، مطالب زیر را اضافه کنید.
// features/sections/SectionOne/SectionOne.tsx
import { IDynamicSectionProps } from “../../../interfaces/IDynamicSection”;
export interface ISectionOne {
heading: string;
}
export const SectionOne = ({
properties,
}: IDynamicSectionProps) => {
const { heading } = properties;
return ;
};
// features/sections/SectionTwo/SectionTwo.tsx
import { IDynamicSectionProps } from “../../../interfaces/IDynamicSection”;
export interface ISectionTwo {
heading: string;
}
export const SectionTwo = ({
properties,
}: IDynamicSectionProps) => {
const { heading } = properties;
return ;
};
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
اکنون که بخشهای خود را اضافه کردهایم، به بخش میرویم فایل فهرست از هر بخش
// features/Sections/SectionOne/index.ts
import { IDynamicSection } from “../../../interfaces/IDynamicSection”
import {SectionOne, ISectionOne} from “./SectionOne”
const sectionOne: IDynamicSection = {
name: “sectionOne”,
displayComponent: SectionOne
}
export default sectionOne
// features/Sections/SectionTwo/index.ts
import { IDynamicSection } from “../../../interfaces/IDynamicSection”
import {SectionTwo, ISectionTwo} from “./SectionTwo”
const sectionTwo: IDynamicSection = {
name: “sectionTwo”,
displayComponent: SectionTwo
}
export default sectionTwo
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
می بینید که ما یک نام برای بخش به همراه قسمت تعریف کرده ایم نمایش کامپوننت ویژگی هایی که به جزء ارجاع می دهند.
سپس باید یک تابع mapper ایجاد کنیم تا بتوانیم بخش را بسته به نام آن بازیابی کنیم.
// features/Sections/index.ts
import {IDynamicSection} from “../../interfaces/IDynamicSection”
import sectionOne from “./SectionOne”
import sectionTwo from “./SectionTwo”
export const appSections: {[key: string]: IDynamicSection} = {
– [sectionOne.name]: sectionOne,
– [sectionTwo.name]: sectionTwo
}
export const findSectionByName = (name: string) => appSections[name];
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
سپس به سراغ DynamicPage جزء، مسئول ارائه هر بخش.
// features/DynamicPage/DynamicPage.tsx
import React from “react”;
import { useLocation, useNavigate, useParams } from “react-router-dom”;
import { IDynamicSectionValues } from “../../interfaces/IDynamicSection”;
import { IDynamicPage } from “../../interfaces/IDynamicPage”;
import { SectionErrorBoundary } from “./SectionErrorBoundary”;
import { findSectionByName } from “../Sections”;
interface RenderSectionErrorProps {
title: string;
message: string;
section: IDynamicSectionValues;
}
const RenderSectionError = (props: RenderSectionErrorProps) => (
{props.title}
{props.message}
Section
{JSON.stringify(props.section, null, 2)}
) صادرات رابط IDynamicPageProps { dynamicPage: IDynamicPage; showHandlerNotFoundError?: boolean; showGenericSectionErrors?: boolean; } export const DynamicPage = (props: IDynamicPageProps) => { const { showHandlerNotFoundError = true, showGenericSectionErrors = true } = props; const navigate = useNavigate(); const location = useLocation(); const params = useParams(); const RenderSection = (بخش: IDynamicSectionValues) => { try { const sectionHandler = findSectionByName(section._type); if (!sectionHandler) { console.error(“SectionHandler for پیدا نشد”, section._type); بازگشت showHandlerNotFoundError ? (
) : خالی؛ } const sectionProps = { navigate, location, params, properties: section.properties || {}، }؛ const SectionHandler = sectionHandler.displayComponent به عنوان React.ElementType. برگشت (
) } catch (error) { console.error(“Error rendering section DynamicPage”, section, error); بازگشت showGenericSectionErrors && error instanceof Error ? (
) : خالی؛ } } برگشت (
{(props.dynamicPage.sections || []).map((بخش، idx) => (
))}
) };
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
کار را با اضافه کردن شروع می کنیم RenderSectionError کامپوننت، کامپوننتی است که در صورت بروز مشکل در رندر یک بخش، نمایش خواهیم داد. این مؤلفه حاوی اطلاعاتی در مورد اینکه کدام بخش در ارائه مشکل دارد، همراه با خروجی JSON مرتبط برای اهداف اشکال زدایی خواهد بود.
بنابراین DynamicPage کامپوننت دارای یک جزء در درون است – RenderSection. این RenderSection کامپوننت، همانطور که از نامش مشخص است، مسئول ارائه یک بخش است و یک بخش-داده بازیابی شده از Sanity را می پذیرد. سپس، در یک بلوک try-catch، از آن استفاده می کنیم بخش یاب نقشه برداری fn برای پیدا کردن بخش خاص
علاوه بر مواردی که برای هر بخش ارسال می کنیم، برخی اطلاعات متا اضافی را برای هر صفحه درج می کنیم. این نمایش کامپوننت سپس از بخش Handler بازیابی و برگردانده می شود.
یک سطح بالاتر از DynamicPage ما در حال حاضر ایجاد شده است DynamicPageWrapper.
// features/DynamicPage/DynamicPageWrapper.tsx
import { useEffect, useState } from “react”;
import { IDynamicRoute } from “../../interfaces/IDynamicRoute”;
import { DynamicPage, IDynamicPageProps } from “./DynamicPage”;
import { NotFound } from “../Layout/NotFound”;
interface IDynamicPageWrapperProps {
route: IDynamicRoute;
}
const DynamicPageWrapper = (props: IDynamicPageWrapperProps) => {
const pageId: string = props.route.properties.page._ref;
const [dynamicPage, setDynamicPage] = useState(null);
const [isLoading, setIsLoading] = useState(true);
async function fetchDynamicPage(id: string) {
return fetch(`http://localhost:3000/api/v1/dynamicPage/${id}`)
.then((response) => response.json())
.then((data) => {
setDynamicPage(data);
})
.finally(() => setIsLoading(false));
}
useEffect(() => {
if (!dynamicPage) {
fetchDynamicPage(pageId);
} else {
setIsLoading(false);
}
}, []);
const RenderPage = () => {
if (isLoading) {
return Loading…;
}
if (!dynamicPage) {
return ;
}
const newProps: IDynamicPageProps = {
dynamicPage,
};
return ;
};
return ;
};
export default DynamicPageWrapper;
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
این کامپوننت به صفحه در حال ارائه اشاره دارد که برای بازیابی محتوای آن از طریق درخواست واکشی استفاده می شود. نتیجه این درخواست همراه با بخش یاب fn سپس به DynamicPage به عنوان لوازم جانبی
محتوای چیدمان پوشه به عنوان بسته بندی ثابت محتوای پویا ما عمل می کند.
// features/Layout/Footer.tsx
export const Footer = () => {
return Static Footer
}
// features/Layout/Header.tsx
export const Header = () => {
return Static Header
}
// features/Layout/MainContainer.tsx
export const MainContainer = () => {
return (
>
)
}
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
و تمام کردن همه چیز است جزء برنامه.
// App.tsx
import { useEffect, useState } from “react”;
import “./App.css”;
import { IDynamicRoute } from “./interfaces/IDynamicRoute”;
import DynamicPageWrapper from “./features/DynamicPage/DynamicPageWrapper”;
import { RouterProvider, createBrowserRouter } from “react-router-dom”;
import { MainContainer } from “./features/Layout/MainContainer”;
import { routesConfig } from “./routes/routesConfig”;
interface IRouteHandlerProps {
route: IDynamicRoute;
}
async function fetchRoutes() {
return fetch(“http://localhost:3000/api/v1/dynamicRoutes”)
.then((response) => response.json())
.then((data) => data)
.catch((error) => console.error(“Error:”, error));
}
function App() {
const [routes, setRoutes] = useState([]);
const renderRoute =
(route: IDynamicRoute): React.ElementType =>
(routeProps: IRouteHandlerProps) => {
return ;
};
const RenderDynamicRoutes = routes.map((route: IDynamicRoute) => {
const currentRoute = route.path.toString();
const RenderedRoute = renderRoute(route);
return {
path: currentRoute,
element: ,
};
});
useEffect(() => {
const fetchData = async () => {
const data = await fetchRoutes();
setRoutes(data.routes);
};
fetchData();
}, []);
return (
,
children: […RenderDynamicRoutes, …routesConfig],
errorElement: ERROR,
},
])}
fallbackElement={
Loading…
}
/>
);
}
export default App;
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
بنابراین، ما مسیرها را از طریق یک درخواست واکشی بازیابی می کنیم، که سپس به حالت جزء اضافه می شود. سپس ما آن را داریم RouteProvider از React Router که از ایجاد مرورگر روتر روشی برای ساخت مسیرهای پویا با استفاده از RenderDynamicRoutes نقشه کش
نام من ماتیس گاربرگ است و من یک مشاور در Dfind Consulting در اسلو، نروژ هستم.
ساختن با لگو همیشه مورد علاقه من بوده است. چیدن یک پازل سه بعدی، چه از روی محدودیت های یک کتابچه راهنمای کاربر یا در حوزه خلاق، ترکیبی منحصر به فرد از تحریک ذهنی و آزمونی از صبر را ارائه می دهد.
این خلاقیت در ساختن ساختارهای مختلف بر اساس یک هسته یک ویژگی مفید در بسیاری از جنبه های زندگی است، چه رسد به توسعه وب. در طول این مقاله، ما بر اساس این مفهوم برای ایجاد یک رابط کاربری کاملا پویا خواهیم بود. مانند مونتاژ بلوکهای لگو، ما بخشهایی از محتوا را گرد هم میآوریم، جایی که شما کنترل کامل نتیجه نهایی را در دست دارید.
مفهوم
بنابراین ایده این است که یک درخواست اولیه برای بازیابی مسیرهای برنامه از Sanity انجام دهیم و به صورت پویا یک روتر React را با آنها پیکربندی کنیم. این مسیرها همراه با نام مسیر حاوی ارجاع به صفحه پویا مورد نظر هستند و برای بازیابی صفحه به همراه بخش های مرتبط با آن استفاده می شود. محتوای ایستا مانند هدر و پاورقی فراتر از خروجی وجود دارد تا بین انتقال مسیر ثابت بماند.
ساختن نمونه سلامتی ما
از آنجایی که تمام محتوای برنامه ما از Sanity ارائه می شود، این مکان منطقی برای شروع است. Sanity یک راه حل محبوب برای ارائه محتوا به برنامه های مصرف کننده به شیوه ای انعطاف پذیر و پویا است. می توانید دستورالعمل های موجود در این صفحه را در مورد نحوه تنظیم آن دنبال کنید.
پس از تکمیل تنظیمات اولیه، ساختار پوشه زیر را در برنامه Sanity اضافه کنید.
|-- ...
|-- schemas
|-- documents
|- application.ts
|-- dynamicPage
|-- sections
|- SectionOne.ts
|- SectionTwo.ts
|-- ...
|- index.ts
|- dynamicPage.ts
|-- objects
|- dynamicRoute.ts
بخش ها همانطور که در تصویر بالا توضیح داده شده است، یک بخش مجزا از محتوا را در صفحه تعریف می کند. این می تواند همه چیز از یک باشد HeroBanner در بالای صفحه به یک عنوان ساده، و مانند این خواهد بود.
// schemas/dynamicPage/sections/sectionOne.ts
export const sectionOne = defineField({
title: "Section One",
name: "sectionOne",
type: "object",
fields: [
defineField({
title: "Properties",
name: "properties",
type: "object",
fields: [
defineField({
title: "Heading",
name: "heading",
type: "string",
description: "The heading for the banner.",
}),
]
})
]
});
// schemas/dynamicPage/sections/sectionTwo.ts
export const sectionTwo = defineField({
title: "Section Two",
name: "sectionTwo",
type: "object",
fields: [
defineField({
title: "Properties",
name: "properties",
type: "object",
fields: [
defineField({
title: "Heading",
name: "heading",
type: "string",
description: "The heading for the banner.",
}),
]
})
]
});
این خواص نامگذاری برای کسانی که پسزمینه React دارند آشنا خواهد بود، زیرا ایده این است که این مقادیر را بهعنوان پایه به کامپوننت مورد نظر منتقل کنند.
سپس باید یک فیلد آرایه برای صادرات این بخش ها ایجاد کنیم. ما همچنین یک را اضافه می کنیم مرتب سازی با یک localCompare fn برای مرتب کردن آنها بر اساس حروف الفبا
// schemas/dynamicPage/sections/sections.ts
import {sectionOne} from "./sectionOne"
import {sectionTwo} from "./sectionTwo"
import {defineField} from "sanity"
const sectionsByName= [
sectionOne,
sectionTwo
].sort((a, b) => a.title!.localeCompare(b.title!))
export const sections = () =>
defineField({
title: "Sections",
name: "sections",
type: "array",
of: sectionsByName,
})
یک صفحه پویا از یک یا چند بخش به هر ترتیب خاص تشکیل شده است و همچنین دارای عنوانی برای سهولت ارجاع است.
// schemas/dynamicPage/dynamicPage.ts
import {sections} from "./sections/sections"
export const dynamicPage = defineField({
title: "Dynamic Pages",
name: "dynamicPage",
type: "document",
fields: [
defineField({
title: "Title",
name: "title",
type: "string"
}),
sections()
]
})
این مسیر پویا شامل تمام مسیرهای برنامه ما با ارجاع به صفحه(های) پویا خواهد بود و در سند برنامه زندگی می کند، که ساختار کل برنامه را مشخص می کند.
// objects/dynamicRoute
export const dynamicRoute = defineField({
title: "Dynamic Route",
name: "dynamicRoute",
type: "object",
fields: [
defineField({
title: "Path",
name: "path",
type: "string",
description:
"The path this route should match. Supports all route expressions that expressjs supports.",
validation: (Rule) =>
Rule.required().custom((path: string | undefined) =>
(path || "").startsWith("https://dev.to/") ? true : "Path must start with /"
),
}),
defineField({
title: "Properties",
name: "properties",
type: "object",
fields: [
defineField({
title: "Reference page",
name: "page",
type: "reference",
to: [{type: "dynamicPage"}],
})
]
}),
]
});
// schemas/documents/application.ts
export const application = defineField({
name: "application",
type: "document",
description: "Describes the central configuration for an application",
fields: [
defineField({
title: "Application name",
name: "name",
type: "string",
validation: (Rule) => Rule.required()
}),
defineField({
title: "Description",
name: "description",
type: "string",
fieldset: "info",
description: "A short description of this application.",
}),
defineField({
title: "Routes",
name: "routes",
type: "array",
of: [dynamicRoute],
options: {
layout: "list",
},
})
]
});
در نهایت، ما باید مدارک خود را در بین آنها ثبت کنیم schemaTypes.
// schemaTypes/index.ts
import { application } from "../schemas/documents/application";
import { dynamicPage } from "../schemas/dynamicPage/dynamicPage";
export const schemaTypes = [application, dynamicPage]
اضافه کردن محتوا
راهاندازی و اجرا کردن نمونه CMS هم خوب و خوب است، اما هنوز باید مقداری محتوا اضافه کنیم.
با افزودن یک سند صفحه پویا با دو بخش شروع کنید – بخش یک و بخش دوم.
سپس باید یک اپلیکیشن ایجاد کنیم و مسیری را با ارجاع به صفحه پویا اضافه کنیم.
در نهایت تغییرات را منتشر کنید.
سرور نود با پرس و جوهای GROQ
اکنون که نمونه Sanity ما به پایان رسیده است، باید محتوا را مصرف کنیم. در اینجا، از یک Node Server با یک کلاینت Sanity برای انجام پرس و جوهای GROQ در مورد نمونه استفاده می کنیم. سپس محتوا را از طریق نقاط پایانی ایجاد شده با مشتری در معرض دید مشتری قرار می دهیم چارچوب اکسپرس.
با مقداردهی اولیه یک برنامه Node جدید شروع کنید و بسته های زیر را نصب کنید.
npm init -y
npm install express @sanity/client cors
از آنجایی که می خواهیم در اینجا نیز از TypeScript استفاده کنیم، باید از طریق دستور زیر آن را در داخل برنامه پیکربندی کنیم و انواع لازم را نصب کنیم.
npx tsc --init
npm i -D typescript ts-node @types/node @types/express @types/cors
به ساختن ساختار زیر در سرور ادامه دهید.
|-- queries
| |- SanityDynamicRoutesQuery.ts
| |- SanityDynamicPageQuery.ts
|- start.ts
این start.ts فایل نقطه ورودی سرور خواهد بود و حاوی نقاط پایانی ما به همراه تنظیمات CORS برای مبدا مشتری است.
به منظور ایجاد پرس و جوهای Sanity قابل اجرا، به برخی تنظیمات اضافی نیاز داریم تا بتوانیم ارتباطی با نمونه خود برقرار کنیم. این با اضافه کردن تنظیمات مورد نظر در داخل به دست می آید createClient روش، و همه در Sanity موجود است.
// start.ts
import {createClient} from '@sanity/client'
import { SanityDynamicPageQuery } from './queries/SanityDynamicPageQuery'
import { SanityDynamicRoutesQuery } from './queries/SanityDynamicRoutesQuery'
import { Request, Response } from 'express'
import cors from "cors";
export const client = createClient({
projectId: 'your-project-id',
dataset: 'your-dataset',
useCdn: false, // set to `false` to bypass the edge cache
apiVersion: '2024-02-29', // use current date (YYYY-MM-DD) to target the latest API version
})
var express = require('express')
var app = express()
const allowedOrigins = ['https://localhost:5173']; // standard Vite port
const options: cors.CorsOptions = {
origin: allowedOrigins
};
app.use(cors(options));
app.get('/api/v1/dynamicRoutes', async function (req: Request, res: Response) {
var dynamicRoutes = await SanityDynamicRoutesQuery();
return res.send(dynamicRoutes);
})
app.get('/api/v1/dynamicPage/:id', async function (req: Request, res: Response) {
var dynamicPage = await SanityDynamicPageQuery(req.params.id)
return res.send(dynamicPage);
})
app.listen(3000, () => {
console.log(`[server]: Server is running at http://localhost:3000`);
})
شروع با SanityDynamicRouteQuery، برای بازیابی داده ها از Sanity باید یک پرس و جو GROQ انجام دهیم. شما می توانید با استفاده از افزونه Sanity Vision در نمونه CMS، این پرسش ها را آزمایش کنید.
درخواست زیر همه مسیرهای پویا منتشر شده برنامه مورد نظر را به استثنای اسناد پیش نویس برمی گرداند.
// queries/SanityDynamicRoutesQuery.ts
export const SanityDynamicRoutesQuery = async () => {
const groqQuery = `
*[!(_id in path('drafts.**')) && _type == "application"][0]{routes}`;
const data = await client.fetch(groqQuery);
return data
}
سپس اضافه کنید SanityDynamicPageQuery فایل. این صفحه پویا را با استفاده از شناسه مرجع از خروجی در پرس و جو می کند SanityDynamicRoutesQuery، و بخش های لیست را با ویژگی های مرتبط آن برگردانید.
// queries/SanityDynamicPageQuery.ts
export const SanityDynamicPageQuery = async (id: string) => {
const groqQuery = `*[
_type == "dynamicPage"
&& (_id == '${id}')]{
id{current},
sections{
_type,
properties,
},
...
}[0]`
const data = await client.fetch(groqQuery);
return data;
}
سرور را راه اندازی کنید و هر دوی این نقاط پایانی را با Postman یا ابزاری مشابه فراخوانی کنید تا مطمئن شوید که همه چیز طبق خواسته کار می کند.
npx ts-node start.ts
[server]: Server is running at http://localhost:3000
ساخت رابط کاربری پویا
با شروع نسخه پایدار اخیر Node، از دستور زیر برای مقداردهی اولیه برنامه خود استفاده می کنیم به سرعت، واکنش نشان دهید و TypesScript.
npm create vite@latest
> React
> TypeScript
پس از اتمام فرمان، با مقداردهی اولیه برنامه، بررسی کنید که همه چیز کار می کند.
npm run dev
این یک سرور توسعه دهنده محلی در http را راه اندازی می کند. برای مدیریت گواهیهای محلی با Vite، از vite-plugin-mkcert با گزینههای پیکربندی زیر استفاده کنید.
ما همچنین از این فرصت برای نصب استفاده می کنیم react-router-dom بسته بندی
npm i react-router-dom
npm i -D vite-plugin-mkcert
// vite.config.ts
import {defineConfig} from'vite'
import mkcert from 'vite-plugin-mkcert'
export default defineConfig({
plugins: [react(), mkcert()]
server: {
https: true
},
})
برنامه bare bone اکنون باید پس از مقداردهی اولیه روی https راهاندازی و اجرا شود و میتوانیم ساختار زیر را اضافه کنیم.
|-- features
|-- DynamicPage
|- DynamicPageWrapper.tsx
|- DynamicPage.tsx
|- SectionErrorBoundary.tsx
|-- Sections
|-- SectionOne
|- SectionOne.tsx
|- SectionOne.module.scss
|- index.ts
|-- SectionTwo
|- SectionTwo.tsx
|- SectionTwo.module.scss
|- index.ts
|-- Layout
|- MainContainer.tsx
|- Footer.tsx
|- Header.tsx
|- NotFound.tsx
|- index.ts
|-- routes
|- routesConfig.ts
|-- interfaces
|- IDynamicSection.ts
|- App.tsx
با شروع از رابطها، باید چندین مورد از آنها را برای مدیریت محتوای پویا اضافه کنیم. این DynamicSectionFinder یک رابط برای یک تابع نگاشت است و در ادامه با جزئیات بیشتر توضیح داده خواهد شد. این IDynamicRoute رابطی است که ما برای مسیرهای پویا استفاده خواهیم کرد و شامل یک مسیر و خصوصیات است. این IDynamicSectionProps، بخش IDynamic و IDynamicSectionValues همه اینترفیسهایی هستند که به بخشهای یک صفحه پویا مربوط میشوند و با پیشروی واضحتر میشوند.
// interfaces/IDynamicSectionFinder
export type DynamicSectionFinder =
(name: string) => IDynamicSectionValues | undefined;
// interfaces/IDynamicRoute
export interface IDynamicRoute {
path: string;
properties: Properties;
}
// interfaces/IDynamicSection
export interface IDynamicSectionProps {
properties: T;
}
// interfaces/IDynamicSection
export interface IDynamicSection {
name: string;
displayComponent: React.ComponentType>;
}
// interfaces/IDynamicSectionValues
export interface IDynamicSectionValues {
_type: string;
properties: Properties;
}
حرکت به سمت routesConfig.ts فایل، محتوای زیر را داخل آن اضافه کنید.
import {NotFound} from "../features/NotFound/NotFound.tsx"
export const routesConfig = [
{
path: "*",
element:
},
]
بله، در حال حاضر فقط یک مسیر، زیرا مسیرهای دیگر به صورت پویا اضافه خواهند شد.
سپس به عمیق ترین سطح ساختار پویا خود یعنی بخش ها می رویم. شروع با بخش یک– و دو، مطالب زیر را اضافه کنید.
// features/sections/SectionOne/SectionOne.tsx
import { IDynamicSectionProps } from "../../../interfaces/IDynamicSection";
export interface ISectionOne {
heading: string;
}
export const SectionOne = ({
properties,
}: IDynamicSectionProps) => {
const { heading } = properties;
return ;
};
// features/sections/SectionTwo/SectionTwo.tsx
import { IDynamicSectionProps } from "../../../interfaces/IDynamicSection";
export interface ISectionTwo {
heading: string;
}
export const SectionTwo = ({
properties,
}: IDynamicSectionProps) => {
const { heading } = properties;
return ;
};
اکنون که بخشهای خود را اضافه کردهایم، به بخش میرویم فایل فهرست از هر بخش
// features/Sections/SectionOne/index.ts
import { IDynamicSection } from "../../../interfaces/IDynamicSection"
import {SectionOne, ISectionOne} from "./SectionOne"
const sectionOne: IDynamicSection = {
name: "sectionOne",
displayComponent: SectionOne
}
export default sectionOne
// features/Sections/SectionTwo/index.ts
import { IDynamicSection } from "../../../interfaces/IDynamicSection"
import {SectionTwo, ISectionTwo} from "./SectionTwo"
const sectionTwo: IDynamicSection = {
name: "sectionTwo",
displayComponent: SectionTwo
}
export default sectionTwo
می بینید که ما یک نام برای بخش به همراه قسمت تعریف کرده ایم نمایش کامپوننت ویژگی هایی که به جزء ارجاع می دهند.
سپس باید یک تابع mapper ایجاد کنیم تا بتوانیم بخش را بسته به نام آن بازیابی کنیم.
// features/Sections/index.ts
import {IDynamicSection} from "../../interfaces/IDynamicSection"
import sectionOne from "./SectionOne"
import sectionTwo from "./SectionTwo"
export const appSections: {[key: string]: IDynamicSection} = {
- [sectionOne.name]: sectionOne,
- [sectionTwo.name]: sectionTwo
}
export const findSectionByName = (name: string) => appSections[name];
سپس به سراغ DynamicPage جزء، مسئول ارائه هر بخش.
// features/DynamicPage/DynamicPage.tsx
import React from "react";
import { useLocation, useNavigate, useParams } from "react-router-dom";
import { IDynamicSectionValues } from "../../interfaces/IDynamicSection";
import { IDynamicPage } from "../../interfaces/IDynamicPage";
import { SectionErrorBoundary } from "./SectionErrorBoundary";
import { findSectionByName } from "../Sections";
interface RenderSectionErrorProps {
title: string;
message: string;
section: IDynamicSectionValues;
}
const RenderSectionError = (props: RenderSectionErrorProps) => (
{props.title}
{props.message}
Section
{JSON.stringify(props.section, null, 2)}
) صادرات رابط IDynamicPageProps { dynamicPage: IDynamicPage; showHandlerNotFoundError?: boolean; showGenericSectionErrors?: boolean; } export const DynamicPage = (props: IDynamicPageProps) => { const { showHandlerNotFoundError = true, showGenericSectionErrors = true } = props; const navigate = useNavigate(); const location = useLocation(); const params = useParams(); const RenderSection = (بخش: IDynamicSectionValues) => { try { const sectionHandler = findSectionByName(section._type); if (!sectionHandler) { console.error("SectionHandler for پیدا نشد", section._type); بازگشت showHandlerNotFoundError ? (
) : خالی؛ } const sectionProps = { navigate, location, params, properties: section.properties || {}، }؛ const SectionHandler = sectionHandler.displayComponent به عنوان React.ElementType. برگشت (
) } catch (error) { console.error("Error rendering section DynamicPage", section, error); بازگشت showGenericSectionErrors && error instanceof Error ? (
) : خالی؛ } } برگشت (
{(props.dynamicPage.sections || []).map((بخش، idx) => (
))}
) };
کار را با اضافه کردن شروع می کنیم RenderSectionError کامپوننت، کامپوننتی است که در صورت بروز مشکل در رندر یک بخش، نمایش خواهیم داد. این مؤلفه حاوی اطلاعاتی در مورد اینکه کدام بخش در ارائه مشکل دارد، همراه با خروجی JSON مرتبط برای اهداف اشکال زدایی خواهد بود.
بنابراین DynamicPage کامپوننت دارای یک جزء در درون است – RenderSection. این RenderSection کامپوننت، همانطور که از نامش مشخص است، مسئول ارائه یک بخش است و یک بخش-داده بازیابی شده از Sanity را می پذیرد. سپس، در یک بلوک try-catch، از آن استفاده می کنیم بخش یاب نقشه برداری fn برای پیدا کردن بخش خاص
علاوه بر مواردی که برای هر بخش ارسال می کنیم، برخی اطلاعات متا اضافی را برای هر صفحه درج می کنیم. این نمایش کامپوننت سپس از بخش Handler بازیابی و برگردانده می شود.
یک سطح بالاتر از DynamicPage ما در حال حاضر ایجاد شده است DynamicPageWrapper.
// features/DynamicPage/DynamicPageWrapper.tsx
import { useEffect, useState } from "react";
import { IDynamicRoute } from "../../interfaces/IDynamicRoute";
import { DynamicPage, IDynamicPageProps } from "./DynamicPage";
import { NotFound } from "../Layout/NotFound";
interface IDynamicPageWrapperProps {
route: IDynamicRoute;
}
const DynamicPageWrapper = (props: IDynamicPageWrapperProps) => {
const pageId: string = props.route.properties.page._ref;
const [dynamicPage, setDynamicPage] = useState(null);
const [isLoading, setIsLoading] = useState(true);
async function fetchDynamicPage(id: string) {
return fetch(`http://localhost:3000/api/v1/dynamicPage/${id}`)
.then((response) => response.json())
.then((data) => {
setDynamicPage(data);
})
.finally(() => setIsLoading(false));
}
useEffect(() => {
if (!dynamicPage) {
fetchDynamicPage(pageId);
} else {
setIsLoading(false);
}
}, []);
const RenderPage = () => {
if (isLoading) {
return Loading...
;
}
if (!dynamicPage) {
return ;
}
const newProps: IDynamicPageProps = {
dynamicPage,
};
return ;
};
return ;
};
export default DynamicPageWrapper;
این کامپوننت به صفحه در حال ارائه اشاره دارد که برای بازیابی محتوای آن از طریق درخواست واکشی استفاده می شود. نتیجه این درخواست همراه با بخش یاب fn سپس به DynamicPage به عنوان لوازم جانبی
محتوای چیدمان پوشه به عنوان بسته بندی ثابت محتوای پویا ما عمل می کند.
// features/Layout/Footer.tsx
export const Footer = () => {
return
}
// features/Layout/Header.tsx
export const Header = () => {
return Static Header
}
// features/Layout/MainContainer.tsx
export const MainContainer = () => {
return (
>
)
}
و تمام کردن همه چیز است جزء برنامه.
// App.tsx
import { useEffect, useState } from "react";
import "./App.css";
import { IDynamicRoute } from "./interfaces/IDynamicRoute";
import DynamicPageWrapper from "./features/DynamicPage/DynamicPageWrapper";
import { RouterProvider, createBrowserRouter } from "react-router-dom";
import { MainContainer } from "./features/Layout/MainContainer";
import { routesConfig } from "./routes/routesConfig";
interface IRouteHandlerProps {
route: IDynamicRoute;
}
async function fetchRoutes() {
return fetch("http://localhost:3000/api/v1/dynamicRoutes")
.then((response) => response.json())
.then((data) => data)
.catch((error) => console.error("Error:", error));
}
function App() {
const [routes, setRoutes] = useState([]);
const renderRoute =
(route: IDynamicRoute): React.ElementType =>
(routeProps: IRouteHandlerProps) => {
return ;
};
const RenderDynamicRoutes = routes.map((route: IDynamicRoute) => {
const currentRoute = route.path.toString();
const RenderedRoute = renderRoute(route);
return {
path: currentRoute,
element: ,
};
});
useEffect(() => {
const fetchData = async () => {
const data = await fetchRoutes();
setRoutes(data.routes);
};
fetchData();
}, []);
return (
,
children: [...RenderDynamicRoutes, ...routesConfig],
errorElement: ERROR
,
},
])}
fallbackElement={
Loading...
}
/>
);
}
export default App;
بنابراین، ما مسیرها را از طریق یک درخواست واکشی بازیابی می کنیم، که سپس به حالت جزء اضافه می شود. سپس ما آن را داریم RouteProvider از React Router که از ایجاد مرورگر روتر روشی برای ساخت مسیرهای پویا با استفاده از RenderDynamicRoutes نقشه کش
نام من ماتیس گاربرگ است و من یک مشاور در Dfind Consulting در اسلو، نروژ هستم.