> 웹 프론트엔드 > JS 튜토리얼 > 추상 팩토리 패턴 마스터하기: 종합 가이드

추상 팩토리 패턴 마스터하기: 종합 가이드

Susan Sarandon
풀어 주다: 2024-12-05 20:41:14
원래의
906명이 탐색했습니다.

논리를 계속해서 복제하지 않고 애플리케이션에서 다양한 객체 계열의 다양한 변형을 생성해야 하는 경우를 발견한 적이 있습니까?

아니면 애플리케이션을 구축했지만 새로운 요구 사항이나 고객의 변경된 선호 사항으로 인해 완전히 새로운 개체가 필요하다는 사실을 깨닫고 전체 코드베이스를 다시 작업해야 했을 수도 있습니다.

새 구현을 연결하는 것만으로 기존 코드를 손상시키지 않고 새로운 변형을 원활하게 도입

할 수 있는 방법이 있다면 어떨까요?

여기서 추상 팩토리

디자인 패턴이 등장합니다!

이 튜토리얼에서는 다양한 형식과 테마를 지원하는 다양한 유형의 이력서를 만들기 위한 Node.js CLI 애플리케이션

을 구축하여 이 강력한 디자인 패턴을 분석해 보겠습니다.

개요

추상 팩토리창조적 디자인 패턴으로, 을 사용하여 객체를 생성하는 기본 방식에서 발생하는 다양한 문제를 다루는 디자인 패턴 카테고리입니다. >new

키워드 또는 연산자.

Abstract Factory

디자인 패턴은 이번 블로그 글에서 다룬 팩토리 메소드 디자인 패턴을 일반화한 것이라고 생각하시면 됩니다.

문제

Abstract Factory

디자인 패턴은 다음 문제를 해결합니다.
  1. PDFResume , JSONResumeMarkdownResume과 같은 관련 제품군
  2. 을 어떻게 만들 수 있나요?
  3. CreativeResume , MinimalistResumeModernResume
  4. 과 같은 제품군당 여러 변형을 지원하려면 어떻게 해야 합니까?
  5. 기존 소비 코드나 클라이언트 코드를 손상하지 않고 더 많은 변형과 제품 추가를 어떻게 지원할 수 있나요?

해결책

추상 팩토리
디자인 패턴은 각 제품 유형에 대한 인터페이스 또는 추상 클래스를 선언하여 이러한 문제를 해결합니다.

export abstract class PDFResume {}
export abstract class JSONResume {}
export abstract class MarkdownResume {}

로그인 후 복사
로그인 후 복사
로그인 후 복사
로그인 후 복사
로그인 후 복사
로그인 후 복사

그런 다음 패턴 이름에서 알 수 있듯이 모든 유형의 제품을 생성하는 팩토리 메소드를 선언하는 인터페이스인 추상 팩토리

를 만듭니다.
  • createPDFResume : PDFResume
  • 유형 또는 하위 유형
  • 을 반환합니다. createMarkdownResume : MarkdownResume
  • 유형 또는 하위 유형을 반환합니다.
  • createJSONResume : JSONResume
  • 유형 또는 하위 유형을 반환합니다.
export interface ResumeFactory {
  createPDFResume(): PDFResume
  createMarkdownResume(): MarkdownResume
  createJSONResume(): JSONResume
}

로그인 후 복사
로그인 후 복사
로그인 후 복사
로그인 후 복사
로그인 후 복사

이제 가능한 모든 유형의 제품을 반품하는 일반 공장이 생겼습니다. 그런데 제품당 여러 변형을 어떻게 지원할 수 있을까요?

답은 추상 팩토리( ResumeFactory )를 구현하는 ConcreteFactory를 만드는 것입니다.

export abstract class PDFResume {}
export abstract class JSONResume {}
export abstract class MarkdownResume {}

로그인 후 복사
로그인 후 복사
로그인 후 복사
로그인 후 복사
로그인 후 복사
로그인 후 복사

이제 클라이언트 클래스에서 팩토리를 사용하려면 ResumeFactory 유형의 변수를 선언한 다음 사용자 입력에 따라 해당 콘크리트 팩토리를 인스턴스화하기만 하면 됩니다.

클라이언트 코드:

export interface ResumeFactory {
  createPDFResume(): PDFResume
  createMarkdownResume(): MarkdownResume
  createJSONResume(): JSONResume
}

로그인 후 복사
로그인 후 복사
로그인 후 복사
로그인 후 복사
로그인 후 복사

구조

Mastering the Abstract Factory Pattern: A Comprehensive Guide

추상 팩토리 디자인 패턴의 구조는 다음 클래스로 구성됩니다.

  1. Factory : 이 디자인 패턴의 이름을 abstract Factory로 명명한 이유는 이 클래스가 모든 ConcreteFactories 간의 계약을 나타내기 때문입니다. 모든 팩토리 메소드를 정의합니다.
  • 팩토리 메소드 수는 제품 수와 같습니다.
  • 팩토리 메소드는 추상 또는 일반 제품 유형( IProduct{j} )을 반환해야 합니다.

이 경우 Factory에 선언된 팩토리 메소드는 createProductA 및 createProductB

입니다.
  1. ConcreteFactory{i} : 이 클래스는 Factory 클래스를 구현하고 각 팩토리 메소드에 대한 사용자 정의 구현을 제공합니다.
  • 위 스키마에서 i는 1 또는 2와 같습니다.
  • ConcreteFactories의 수는 제품당 가능한 변형 수와 같습니다.
  • 각각의 구체적인 팩토리 메소드는 해당 제품의 인스턴스인 객체를 반환해야 합니다.
  1. IProduct{j} : 이 클래스는 추상 제품 유형에 해당합니다.
  • 위 스키마에서 j는 A 또는 B와 같습니다.
  • IProduct{j}는 다양한 구체적인 제품 클래스로 구현됩니다.

ConcretProductA1과 ConcretProductA2는 IProductA를 구현합니다. ConcretProductB1과 ConcretProductB2는 IProductB를 구현합니다.

  1. ConcreteProductsIProduct{j} 일반 유형
  2. 중 하나를 구현한 제품입니다.

실제 시나리오

이 섹션에서는 사용자가 선택한 테마와 형식을 기반으로 이력서를 생성하는 완벽하게 작동하는 Node.js TypeScript CLI 애플리케이션을 구축하여 이전 예를 실행해 보겠습니다.

이 저장소를 컴퓨터에 복제하여 전체 작동 코드를 자유롭게 확인해 보세요.

그런 다음 다음 명령을 실행하세요.

export abstract class PDFResume {}
export abstract class JSONResume {}
export abstract class MarkdownResume {}

로그인 후 복사
로그인 후 복사
로그인 후 복사
로그인 후 복사
로그인 후 복사
로그인 후 복사

유형 선언

유형 안전성을 보장하기 위해 튜토리얼 전체에서 사용할 유형을 선언하는 것부터 시작하겠습니다.

인터페이스/유형

export interface ResumeFactory {
  createPDFResume(): PDFResume
  createMarkdownResume(): MarkdownResume
  createJSONResume(): JSONResume
}

로그인 후 복사
로그인 후 복사
로그인 후 복사
로그인 후 복사
로그인 후 복사
  1. ResumeData 유형은 이름, 이메일, 전화번호, 경험 배열 등 이력서 개체의 모든 속성을 정의합니다.
  2. 경험 유형은 회사, 직위, 시작일, 종료일, 설명으로 구성됩니다.

추상 팩토리 선언

이제 지원되는 다양한 제품 유형에 해당하는 세 가지 팩토리 메소드를 정의하는 일반 팩토리 유형을 선언해 보겠습니다. PDFResume , MarkdownResumeJSONResume.

인터페이스/ResumeFactory

export class CreativeResumeFactory implements ResumeFactory {
  createPDFResume(): CreativePDFResume {
    return new CreativePDFResume() // CreativePDFResume implements PDFResume
  }

  createMarkdownResume(): CreativeMarkdownResume {
    return new CreativeMarkdownResume() // CreativeMarkdownResume implements MarkdownResume
  }

  createJSONResume(): CreativeJSONResume {
    return new CreativeJSONResume() // CreativeJSONResume implements JSONResume
  }
}

로그인 후 복사
로그인 후 복사
로그인 후 복사

다음 섹션에서 코드를 살펴보겠습니다.

다양한 유형의 문서에 대한 공유 클래스 선언

다음으로 일반 제품 클래스를 생성해 보겠습니다.

해당 하위 유형 간에 속성과 메서드를 모두 공유하기를 원하기 때문에 모든 제품 유형은 추상 클래스가 됩니다.

  1. JSONResume: 클래스에는 style이라는 추가 속성과 함께 ResumeData 유형의 객체를 저장하는 보호된 data 속성이 있습니다.

클래스 정의:

  • data 속성에 액세스하기 위한 getter 메소드
  • 나중에 하위 클래스에 의해 재정의될 추상 generate 메서드입니다.
  • JSON 파일에 이력서 데이터를 저장하는 것으로 구성된 기본 구현이 포함된 saveToFile 메소드.

이력서/json/JSONResume

// User inputs...
let theme = "minimalist"
let format = "pdf"

let factory: ResumeFactory

switch (theme) {
  case "minimalist":
    factory = new MinimalistResumeFactory()
    break
  case "modern":
    factory = new ModernResumeFactory()
    break
  case "creative":
    factory = new CreativeResumeFactory()
    break
  default:
    throw new Error("Invalid theme.")
}

const userInput = await getUserInput()
let resume

switch (format) {
  case "pdf":
    resume = factory.createPDFResume()
    break
  case "markdown":
    resume = factory.createMarkdownResume()
    break
  case "json":
    resume = factory.createJSONResume()
    break
  default:
    throw new Error("Invalid format.")
}

로그인 후 복사
로그인 후 복사
로그인 후 복사

추상 키워드는 클래스가 인스턴스화할 수 없는 일반 유형임을 의미합니다. 다른 클래스에서만 상속할 수 있습니다.

  1. MarkdownResume : 클래스에는 마크다운 문자열을 저장하는 보호된 content 속성이 있습니다.

클래스 정의:

  • content 속성에 액세스하기 위한 getter 메소드입니다.
  • 나중에 하위 클래스에 의해 재정의될 추상 generate 메서드입니다.
  • fileName을 가져온 다음 마크다운 형식의 문자열 content를 파일에 저장하는 saveToFile 메서드입니다.

이력서/markdown/MarkdownResume

export abstract class PDFResume {}
export abstract class JSONResume {}
export abstract class MarkdownResume {}

로그인 후 복사
로그인 후 복사
로그인 후 복사
로그인 후 복사
로그인 후 복사
로그인 후 복사
  1. PDF이력서 :

클래스에는 pdfkit이라는 라이브러리에서 가져온 PDFKit.PDFDocument 유형의 보호된 doc 개체가 있습니다. 라이브러리는 객체 지향 인터페이스를 통해 PDF 문서 작성 및 조작을 단순화합니다.

클래스 정의:

  • doc 속성에 액세스하기 위한 getter 메소드
  • 나중에 하위 클래스에 의해 재정의될 추상 generate 메서드입니다.
  • doc 메모리 내 PDF 객체를 특정 파일에 저장하는 saveToFile 메소드입니다.

이력서/pdf/PDFResume

export interface ResumeFactory {
  createPDFResume(): PDFResume
  createMarkdownResume(): MarkdownResume
  createJSONResume(): JSONResume
}

로그인 후 복사
로그인 후 복사
로그인 후 복사
로그인 후 복사
로그인 후 복사

콘크리트 공장 선언

이제 일반 제품 유형추상 팩토리를 정의했으므로 이제 ConcreteFactories 생성을 진행할 차례입니다. 모든 일반 제품 유형의 다양한 변형.

이력서에는 창의적, 미니멀리스트, 현대적의 3가지 변형이 있습니다. 그리고 3가지 유형의 일반 제품: JSON , PDFMarkdown.

추상 팩토리( ResumeFactory )는 제품 생성을 담당하는 3가지 팩토리 메소드를 정의합니다.

  • createPDFResume : PDFResume.
  • 유형의 인스턴스를 생성합니다.
  • createMarkdownResume : MarkdownResume.
  • 유형의 인스턴스를 생성합니다.
  • createJSONResume : JSONResume 유형의 인스턴스를 생성합니다.

제품당 다양한 변형을 지원하려면 콘크리트 공장을 3개 만들어야 합니다.

콘크리트 공장에서는 고유한 맛을 지닌 3가지 유형의 제품을 제작합니다.

  1. CreativeResumeFactory는 Creative 변형 제품을 생성합니다.
  2. MinimalistResumeFactory는 Minimalist 변형 제품을 만듭니다.
  3. ModernResumeFactory는 Modern 변형 제품을 만듭니다.

공장/CreativeResumeFactory

export class CreativeResumeFactory implements ResumeFactory {
  createPDFResume(): CreativePDFResume {
    return new CreativePDFResume() // CreativePDFResume implements PDFResume
  }

  createMarkdownResume(): CreativeMarkdownResume {
    return new CreativeMarkdownResume() // CreativeMarkdownResume implements MarkdownResume
  }

  createJSONResume(): CreativeJSONResume {
    return new CreativeJSONResume() // CreativeJSONResume implements JSONResume
  }
}

로그인 후 복사
로그인 후 복사
로그인 후 복사
  • CreativeResumeFactory 팩토리 메소드는 모든 유형의 제품에 대해 창의적인 콘크리트 제품 ​​변형을 반환합니다.

공장/MinimalistResumeFactory

// User inputs...
let theme = "minimalist"
let format = "pdf"

let factory: ResumeFactory

switch (theme) {
  case "minimalist":
    factory = new MinimalistResumeFactory()
    break
  case "modern":
    factory = new ModernResumeFactory()
    break
  case "creative":
    factory = new CreativeResumeFactory()
    break
  default:
    throw new Error("Invalid theme.")
}

const userInput = await getUserInput()
let resume

switch (format) {
  case "pdf":
    resume = factory.createPDFResume()
    break
  case "markdown":
    resume = factory.createMarkdownResume()
    break
  case "json":
    resume = factory.createJSONResume()
    break
  default:
    throw new Error("Invalid format.")
}

로그인 후 복사
로그인 후 복사
로그인 후 복사
  • MinimalistResumeFactory 팩토리 메소드는 모든 제품 유형에 대해 미니멀리스트 콘크리트 제품 ​​변형을 반환합니다.

공장/ModernResumeFactory

export abstract class PDFResume {}
export abstract class JSONResume {}
export abstract class MarkdownResume {}

로그인 후 복사
로그인 후 복사
로그인 후 복사
로그인 후 복사
로그인 후 복사
로그인 후 복사
  • ModernResumeFactory 팩토리 메소드는 모든 제품 유형에 대해 현대적인 콘크리트 제품 ​​변형을 반환합니다.

크리에이티브 이력서 공장 콘크리트 제품

이제 CreativeResumeFactory에서 반환되는 이전 ConcreteProducts

를 만들어 보겠습니다.

PDF 이력서 :

이력서/pdf/CreativePDFResume

export interface ResumeFactory {
  createPDFResume(): PDFResume
  createMarkdownResume(): MarkdownResume
  createJSONResume(): JSONResume
}

로그인 후 복사
로그인 후 복사
로그인 후 복사
로그인 후 복사
로그인 후 복사

마크다운 이력서 :

이력서/markdown/CreativeMarkdownResume

export class CreativeResumeFactory implements ResumeFactory {
  createPDFResume(): CreativePDFResume {
    return new CreativePDFResume() // CreativePDFResume implements PDFResume
  }

  createMarkdownResume(): CreativeMarkdownResume {
    return new CreativeMarkdownResume() // CreativeMarkdownResume implements MarkdownResume
  }

  createJSONResume(): CreativeJSONResume {
    return new CreativeJSONResume() // CreativeJSONResume implements JSONResume
  }
}

로그인 후 복사
로그인 후 복사
로그인 후 복사

JSON 이력서 :

이력서/json/CreativeJSONResume

// User inputs...
let theme = "minimalist"
let format = "pdf"

let factory: ResumeFactory

switch (theme) {
  case "minimalist":
    factory = new MinimalistResumeFactory()
    break
  case "modern":
    factory = new ModernResumeFactory()
    break
  case "creative":
    factory = new CreativeResumeFactory()
    break
  default:
    throw new Error("Invalid theme.")
}

const userInput = await getUserInput()
let resume

switch (format) {
  case "pdf":
    resume = factory.createPDFResume()
    break
  case "markdown":
    resume = factory.createMarkdownResume()
    break
  case "json":
    resume = factory.createJSONResume()
    break
  default:
    throw new Error("Invalid format.")
}

로그인 후 복사
로그인 후 복사
로그인 후 복사

미니멀리스트 이력서 공장 콘크리트 제품

다음으로 MinimalistResumeFactory에서 반환되는 이전 ConcreteProducts

를 만들어 보겠습니다.

PDF 이력서 :

이력서/pdf/MinimalistPDFResume

npm install
npm start

로그인 후 복사

마크다운 이력서 :

이력서/markdown/MinimalistMarkdownResume

export type ResumeData = {
  name: string
  email: string
  phone: string
  experience: Experience[]
}

export type Experience = {
  company: string
  position: string
  startDate: string
  endDate: string
  description: string
}

로그인 후 복사

JSON 이력서 :

이력서/json/MinimalistJSONResume

import { JSONResume } from "../resumes/json/JSONResume"
import { MarkdownResume } from "../resumes/markdown/MarkdownResume"
import { PDFResume } from "../resumes/pdf/PdfResume"

export interface ResumeFactory {
  createPDFResume(): PDFResume
  createMarkdownResume(): MarkdownResume
  createJSONResume(): JSONResume
}

로그인 후 복사

현대 이력서 공장 콘크리트 제품

마지막으로 ModernResumeFactory에서 반환되는 이전 ConcreteProducts

를 만들어 보겠습니다.

PDF 이력서 :

이력서/pdf/ModernPDFResume

import * as fs from "fs/promises"

import { ResumeData } from "../../interfaces/Types"

export abstract class JSONResume {
  protected data!: ResumeData & { style: string }

  abstract generate(data: ResumeData): void

  async saveToFile(fileName: string): Promise<void> {
    await fs.writeFile(fileName, JSON.stringify(this.data, null, 2))
  }

  getData(): any {
    return this.data
  }
}

로그인 후 복사

마크다운 이력서 :

이력서/markdown/ModernMarkdownResume

import * as fs from "fs/promises"

import { ResumeData } from "../../interfaces/Types"

export abstract class MarkdownResume {
  protected content: string = ""

  abstract generate(data: ResumeData): void

  async saveToFile(fileName: string): Promise<void> {
    await fs.writeFile(fileName, this.content)
  }

  getContent(): string {
    return this.content
  }
}

로그인 후 복사

JSON 이력서 :

이력서/json/ModernJSONResume

import * as fs from "fs"

import PDFDocument from "pdfkit"

import { ResumeData } from "../../interfaces/Types"

export abstract class PDFResume {
  protected doc: PDFKit.PDFDocument

  constructor() {
    this.doc = new PDFDocument()
  }

  abstract generate(data: ResumeData): void

  async saveToFile(fileName: string): Promise<void> {
    const stream = fs.createWriteStream(fileName)
    this.doc.pipe(stream)
    this.doc.end()

    await new Promise<void>((resolve, reject) => {
      stream.on("finish", resolve)
      stream.on("error", reject)
    })
  }

  getBuffer(): Buffer {
    return this.doc.read() as Buffer
  }
}

로그인 후 복사

Index.ts 파일에서 팩토리 사용

클라이언트 코드에서 팩토리를 사용하여 이전 작업의 결실을 맺어 보겠습니다.

이제 공장을 사용하여 이력서 작성 라이브러리를 매우 깔끔한 방식으로 사용할 수 있는 방법을 살펴보세요.

사용자는 다음 두 가지 사항만 제공하면 됩니다.

  1. 제품 유형 : 어떤 유형의 PDF를 만들고 싶나요?
  2. 테마 : 그가 선호하는 이력서 스타일은?

index.ts

import { ResumeFactory } from "../interfaces/ResumeFactory"
import { CreativeJSONResume } from "../resumes/json/CreativeJSONResume"
import { CreativeMarkdownResume } from "../resumes/markdown/CreativeMarkdownResume"
import { CreativePDFResume } from "../resumes/pdf/CreativePDFResume"

export class CreativeResumeFactory implements ResumeFactory {
  createPDFResume(): CreativePDFResume {
    return new CreativePDFResume() // CreativePDFResume extends PDFResume
  }

  createMarkdownResume(): CreativeMarkdownResume {
    return new CreativeMarkdownResume() // CreativeMarkdownResume extends MarkdownResume
  }

  createJSONResume(): CreativeJSONResume {
    return new CreativeJSONResume() // CreativeJSONResume extends JSONResume
  }
}

로그인 후 복사

위 코드는 세 단계로 작동합니다.

  1. 사용자 입력: 먼저 테마형식 값을 가져옵니다.
  2. 팩토리 선택 : 그런 다음 테마 값을 기준으로 해당 팩토리를 인스턴스화합니다.
  3. 제품 생성 : 마지막으로 선택한 형식에 따라 해당 팩토리 메소드를 호출합니다.

사용자는 제품과 해당 변형이 어떻게 생성되는지에 관심이 없습니다. 테마형식만 선택하면, 요청한 대로 해당 제품이 생성됩니다.

이제 클라이언트 코드는 변경 사항에 대해 강력합니다. 새로운 테마나 스타일을 추가하고 싶다면 이를 담당하는 새로운 공장을 만들면 됩니다.

분필 라이브러리를 사용하여 의미론적 의미에 따라 터미널 로그에 색상을 지정했습니다.

CLI 앱 사용자로부터 입력을 얻을 수 있도록 우리는 사용자로부터 다양한 유형의 입력을 얻을 수 있는 매우 매력적이고 사용자 친화적인 방법을 제공하는 inquirer 패키지를 사용했습니다.

  1. 이름, 이메일, 전화번호 등 주요 이력서 정보를 가져오는 데 getUserInput 함수가 사용되었습니다.
  2. getExperience 유틸리티 함수를 사용하여 사용자로부터 경험 정보를 재귀적으로 검색했습니다. 즉, 사용자에게 첫 번째 항목에 대한 경험 정보를 입력하라는 메시지를 표시한 다음 추가할 다른 경험이 있는지 묻습니다. 대답이 '아니요'이면 함수는 그냥 반환됩니다. 반면에 '예'를 선택하면 다음 체험 정보를 입력하라는 메시지가 다시 표시됩니다.

유틸리티/userInput

export abstract class PDFResume {}
export abstract class JSONResume {}
export abstract class MarkdownResume {}

로그인 후 복사
로그인 후 복사
로그인 후 복사
로그인 후 복사
로그인 후 복사
로그인 후 복사

결론

추상 팩토리 패턴은 소프트웨어 디자이너와 개발자에게 강력한 도구입니다. 구체적인 클래스를 지정하지 않고 관련 객체의 패밀리를 생성하는 구조화된 접근 방식을 제공합니다. 이 패턴은 다음과 같은 경우에 특히 유용합니다.

  1. 시스템은 제품이 생성, 구성, 표현되는 방식과 독립적이어야 합니다.
  2. 시스템은 여러 제품군 중 하나로 구성되어야 합니다.
  3. 관련된 제품 개체군은 함께 사용되도록 설계되었으며 이 제약 조건을 적용해야 합니다.
  4. 제품의 클래스 라이브러리를 제공하고 구현이 아닌 인터페이스만 공개하려고 합니다.

실제 예에서는 Abstract Factory 패턴을 적용하여 유연하고 확장 가능한 이력서 생성 시스템을 만드는 방법을 살펴보았습니다. 이 시스템은 기존 코드를 수정하지 않고도 새로운 이력서 스타일이나 출력 형식을 쉽게 수용할 수 있어 개방/폐쇄 원칙의 강력한 효과를 실제로 보여줍니다.

추상 팩토리 패턴은 많은 이점을 제공하지만 코드베이스에 추가적인 복잡성을 초래할 수 있다는 점에 유의하는 것이 중요합니다. 따라서 특정 사용 사례에 제공되는 유연성이 필요한지 여부를 평가하는 것이 중요합니다.

추상 팩토리와 같은 디자인 패턴을 마스터하면 강력하고 유연하며 유지 관리가 가능한 소프트웨어 시스템을 만들 수 있는 능력이 향상됩니다. 소프트웨어 설계 기술을 향상하려면 프로젝트에서 이러한 패턴을 계속 탐색하고 적용하세요.

연락하다

질문이 있으시거나 추가 논의를 원하시면 여기로 연락주세요.

즐거운 코딩하세요!

위 내용은 추상 팩토리 패턴 마스터하기: 종합 가이드의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

원천:dev.to
본 웹사이트의 성명
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.
저자별 최신 기사
인기 튜토리얼
더>
최신 다운로드
더>
웹 효과
웹사이트 소스 코드
웹사이트 자료
프론트엔드 템플릿