برنامه نویسی

سطح سبد خود را بالا ببرید: ساختن یک ترمینال کشیدن و رها کردن در React!

من از به اشتراک گذاشتن آخرین نقطه عطف در سفر خود برای ایجاد نمونه کارها محیط دسک تاپ نهایی هیجان زده ام! من به تازگی اجرای یک ترمینال کاملاً کاربردی را با ویندوزهای قابل کشیدن و قابل انعطاف ، که همه با کد سفارشی تهیه شده اند ، به پایان رسانده ام.

تصویری که یک رابط ترمینال با دستورات متن و خروجی را نشان می دهد.

رمز جیتوب
پیش نمایش زنده

این ویژگی به کاربران امکان می دهد با روشی آشنا و بصری با سیستم عامل شبیه سازی شده ارتباط برقرار کنند. من حتی برخی از دستورات ساختگی را نیز درج کرده ام تا طعم آنچه ممکن است را به شما ارائه دهم.

ساختن یک محیط دسک تاپ واقع بینانه برای یک وب سایت نمونه کارها هیچ شاهکار کوچکی نیست و اضافه کردن یک ترمینال کاربردی آن را به یک سطح کاملاً جدید می برد. این ترمینال فقط یک عنصر استاتیک نیست. این یک مؤلفه پویا و تعاملی است که به کاربران امکان می دهد با سیستم عامل شبیه سازی شده کاوش و تعامل داشته باشند.

شروع کار: تنظیم پروژه بعدی خود

قبل از غواصی به کد ترمینال ، بیایید از طریق راه اندازی اولیه قدم بزنیم. ما از Next.js برای ویژگی های قوی آن و تجربه عالی توسعه دهنده استفاده خواهیم کرد.

  1. ایجاد یک برنامه جدید Next.js:

    ترمینال خود را باز کنید و دستور زیر را اجرا کنید تا یک پروژه Next.js جدید ایجاد کنید:

    npx create-next-app my-terminal-app
    cd my-terminal-app
    
    

تصویری از یک ترمینال در طول تنظیمات برنامه بعدی. JS ، نمایش دستور

This will set up a basic Next.js project structure for you.
حالت تمام صفحه را وارد کنید

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

  1. بسته های مورد نیاز را نصب کنید:

    ما به یک بسته برای رسیدگی به کشش و رها کردن و نوع تعاریف نیاز داریم. آنها را با استفاده از NPM یا نخ نصب کنید:

    npm install react-rnd
    npm install -D @types/node @types/react @types/react-dom typescript
    
    
  • React-RND: این بسته عملکرد قدرتمند و انعطاف پذیر کشش و را فراهم می کند. این مناسب برای ایجاد پنجره های قابل انعطاف و متحرک است که برای ترمینال ما ضروری است.
  • @Types/*: این بسته ها تعاریف نوع TypeScript را برای Node.js ، React و React Dom ارائه می دهند. استفاده از TypeScript با افزودن تایپ استاتیک ، کیفیت و قابلیت حفظ کد را بهبود می بخشد.

  • TypeScript: برای پشتیبانی از Typescript مورد نیاز است.

نکته مهم در مورد next/dynamic:

در next/dynamic Import یک ویژگی داخلی Next.js است ، نه یک بسته جداگانه. این امکان را به ما می دهد تا به صورت پویا مؤلفه ها را وارد کنیم ، که برای ارائه مشتری در سمت بسیار مهم است. از آنجا که مؤلفه ترمینال ما به API های خاص مرورگر متکی است ، ما باید اطمینان حاصل کنیم که روی سرور ارائه نشده است.

غواصی به کد: جادوی ترمینال واکنش نشان داده شده

بیایید نگاهی به کد React JS که این ترمینال را قدرت می دهد ، نگاهی بیندازیم. ما در حال اهرم هستیم next.js برای برنامه ما ، react-rnd برای عملکرد کشیدن و Resize و یک عرف TerminalComponent برای رسیدگی به منطق ترمینال.

pages.tsx (صفحه اصلی):

"use client";
import dynamic from "next/dynamic";
import { Rnd } from "react-rnd";

const TerminalComponent = dynamic(() => import("./components/Terminal"), {
  ssr: false, // Important: Disable server-side rendering
});

export default function Home() {
  return (
    
  );
}

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

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

در اینجا ، ما استفاده می کنیم react-rnd برای اینکه پنجره ترمینال قابل کشیدن و قابل تنظیم باشد. در dynamic واردات تضمین می کند TerminalComponent فقط در سمت مشتری بارگذاری می شود ، که بسیار مهم است زیرا به API های خاص مرورگر متکی است.

index.tsx (مؤلفه ترمینال):

// components/Terminal.tsx
"use client";
import React, { useEffect, useRef, useState } from "react";
import type { AvailableCommands, NestedCommands } from "../data/command";

const BashTerminal: React.FC = () => {
  // 1. Importing Dependencies and Defining Types:
  // (Import statements already present)

  // 2. Initializing State Variables:
  const [cmd, setCmd] = useState("");
  const [output, setOutput] = useState("");
  const [history, setHistory] = useState([]);
  const terminalRef = useRef(null);
  const [nestedMode, setNestedMode] = useState(null);

  const hostname = "terminal";
  const username = "terminal";
  const [directory, setDirectory] = useState("~");

  // 3. Utility Functions:
  const print = (text: string, currentOutput: string): string => {
    return currentOutput + text;
  };

  const command = (outputText: string, currentOutput: string): string => {
    return print(`${outputText}\n${username}@${hostname} ${directory} $ `, currentOutput);
  };

  const empty = (currentOutput = ""): string => {
    return print(`${username}@${hostname} ${directory} $ `, currentOutput);
  };

  const setup = (): string => {
    return empty();
  };

  const cd = (dir: string, param: string | undefined): string => {
    if (param === undefined) {
      return "~";
    }
    if (param.charAt(0) === "https://dev.to/") {
      return param;
    }
    return `${dir}/${param}`;
  };

  // 4. Command Definitions:
  const availableCommands: AvailableCommands = {
    pwd: () => directory,
    cd: (tokens) => {
      setDirectory(cd(directory, tokens[1]));
      return null;
    },
    echo: (tokens) => tokens.slice(1).join(" "),
    clear: () => ({ clear: true }),
    history: () => history.join("\n"),
    help: () => "Available commands: clear, echo, cd, pwd, history, help, mycommand",
    mycommand: () => {
      setNestedMode("mycommand");
      return "Entered mycommand mode. Type 'list', 'info', or 'exit'.";
    },
  };

  const nestedCommands: NestedCommands = {
    mycommand: {
      list: () => "Item 1, Item 2, Item 3",
      info: () => "This is info within mycommand.",
      exit: () => {
        setNestedMode(null);
        return `\n${username}@${hostname} ${directory} $ `;
      },
    },
  };

  // 5. Command Execution:
  const run = async (cmd: string): Promise => {
    const tokens = cmd.split(" ");
    const commandName = tokens[0];

    if (nestedMode) {
      if (nestedCommands[nestedMode] && commandName in nestedCommands[nestedMode]) {
        const nestedModeObject = nestedCommands[nestedMode];
        if (typeof nestedModeObject === "object" && nestedModeObject !== null && commandName in nestedModeObject) {
          return nestedModeObject[commandName as keyof typeof nestedModeObject]();
        }
      }
      return `Command not found in ${nestedMode}: ${commandName}`;
    }

    if (commandName in availableCommands) {
      const result = availableCommands[commandName as keyof typeof availableCommands](tokens);
      if (result instanceof Promise) {
        return await result;
      }
      return result;
    }

    return commandName ? `Command not found: ${commandName}` : "";
  };

  // 6. Effect Hooks:
  useEffect(() => {
    setOutput(setup());
    terminalRef.current?.focus();
  }, []);

  useEffect(() => {
    if (terminalRef.current) {
      terminalRef.current.scrollTo({
        top: terminalRef.current.scrollHeight,
        behavior: "smooth",
      });
    }
  }, [output]);

  // 7. Event Handling:
  const handleKeyDown = async (e: React.KeyboardEvent) => {
    if (e.ctrlKey && e.shiftKey && e.key === "V") {
      e.preventDefault();
      navigator.clipboard.readText().then(text => setCmd(prev => prev + text)).catch(err => {
        console.error("Clipboard access failed:", err);
        alert("Clipboard access denied. Please check your browser permissions.");
      });
      return;
    }

    if (e.key === "Backspace") {
      e.preventDefault();
      setCmd(prev => prev.slice(0, -1));
    } else if (e.key === "Enter") {
      e.preventDefault();
      const cmdToRun = cmd.trim();
      if (cmdToRun) {
        setHistory(prev => [...prev, cmdToRun]);
        const result = await run(cmdToRun.toLowerCase());
        setOutput(prev => {
          const commandLine = `${username}@${hostname} ${directory} $ ${cmdToRun}`;
          let resultOutput: string | { clear: boolean } | null = "";
          if (result === null) resultOutput = `${username}@${hostname} ${directory} $ `;
          else if (typeof result === "object" && result.clear) return empty();
          else resultOutput = typeof result === "string" && result.includes("\n") ? result : `\n${command(typeof result === "string" ? result : "", "")}`;
          const lastPromptIndex = prev.lastIndexOf(`${username}@${hostname} ${directory} $ `);
          const cleanedPrev = lastPromptIndex !== -1 ? prev.substring(0, lastPromptIndex) : prev;
          return cleanedPrev + commandLine + (typeof resultOutput === "string" ? resultOutput : "");
        });
      } else setOutput(prev => empty(prev));
      setCmd("");
    } else if (e.key.length === 1 && !e.ctrlKey && !e.metaKey) setCmd(prev => prev + e.key);
  };

  // 8. Rendering the Terminal:
  return (
    
  );
};

export default BashTerminal;

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

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

commands.tsx (انواع فرمان):

interface AvailableCommands {
  pwd: () => string;
  cd: (tokens: string[]) => string | null;
  echo: (tokens: string[]) => string;
  clear: () => { clear: boolean };
  history: () => string;
  help: () => string;
  mycommand: () => string;
}

interface NestedCommands {
  mycommand: {
    list: () => string;
    info: () => string;
    exit: () => string;
  };
}

export type { AvailableCommands, NestedCommands };

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

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

این پرونده انواع دستورات ما را تعریف می کند و از وضوح نوع و وضوح کد اطمینان می دهد.

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

  • رندر سمت مشتری با next/dynamic: همانطور که قبلاً ذکر شد ، next/dynamic با ssr: false برای مؤلفه ترمینال ما بسیار مهم است. API های خاص مرورگر مانند دستکاری DOM فقط در سمت مشتری در دسترس هستند.
  • کشیدن و رزرو با react-rnd: react-rnd فرآیند ایجاد عناصر قابل انعطاف و قابل انعطاف را ساده می کند. API بصری و گزینه های سفارشی سازی گسترده آن ، آن را به یک انتخاب عالی برای ویندوزهای ترمینال ما تبدیل می کند.
  • مدیریت دولت با useState: ما استفاده می کنیم useState برای مدیریت وضعیت ترمینال ، از جمله ورودی فرمان ، خروجی و تاریخ. مدیریت حالت React تضمین می کند که ترمینال به درستی در پاسخ به تعامل کاربر به روز می شود.
  • دستکاری دام با useRef: useRef به ما اجازه می دهد تا برای پیمایش و مدیریت تمرکز به عنصر DOM ترمینال دسترسی پیدا کنیم. این برای ارائه یک تجربه کاربر صاف و پاسخگو ضروری است.
  • رسیدگی به رویداد با onKeyDown: ما استفاده می کنیم onKeyDown برای گرفتن ورودی صفحه کلید ، کاربران را قادر می سازد تا دستورات را تایپ کرده و با ترمینال تعامل برقرار کنند.
  • TypeScript برای نوع ایمنی نوع: با استفاده از TypeScript و Type Lefinitions ، ما اطمینان حاصل می کنیم که کد ما از نوع ایمن است ، خطر خطاهای زمان اجرا و بهبود قابلیت حفظ کد را کاهش می دهد.
  • ادغام کلیپ بورد: اضافه شده Ctrl+Shift+V برای خمیر از کلیپ بورد.

چرا این بسته ها؟

  • react-rnd: من انتخاب کردم react-rnd به دلیل سهولت در استفاده و انعطاف پذیری. این یک روش ساده و کارآمد برای افزودن قابلیت های کشیدن و رزرو به مؤلفه های React ، که برای ایجاد یک محیط دسک تاپ واقع بینانه ضروری است ، فراهم می کند.
  • TypeScript: من برای بهبود کیفیت کد و قابلیت حفظ کد تصمیم گرفتم. تایپ استاتیک آن به گرفتن خطاها زودتر کمک می کند و باعث می شود که پایگاه کد درک و بازپرداخت آن آسان شود.

به جلو نگاه می کنید: بعد چیست؟

برای به روزرسانی های بیشتر با ما در ارتباط باشید زیرا من همچنان به ساخت وب سایت نمونه کارها “بز” ادامه می دهم! از دیدن چه ویژگی هایی بیشتر هیجان زده اید؟ من قصد دارم عناصر تعاملی بیشتری را اضافه کنم ، سیستم عامل شبیه سازی شده را تقویت کنم و تجربه کلی کاربر را اصلاح کنم.

برخی از ویژگی های بالقوه که من در نظر دارم عبارتند از:

  • اجرای یک سیستم فایل و Explorer File.
  • اضافه کردن دستورات و برنامه های پیچیده تر.
  • ایجاد یک محیط دسک تاپ بصری جذاب تر.
  • اضافه کردن دستورات بیشتر تو در تو.
  • اضافه کردن دستورات بیشتر به دستورات موجود.

به جلو نگاه می کنید: بعد چیست؟

برای به روزرسانی های بیشتر با ما در ارتباط باشید زیرا من به ساخت وب سایت نمونه کارها “بز” ادامه می دهم! از دیدن چه ویژگی هایی بیشتر هیجان زده اید؟

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

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

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

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