Xây Dựng Trình Soạn Thảo Văn Bản Phong Phú (Rich Text Editor) Mạnh Mẽ Trong Ứng Dụng React Của Bạn

Chỉnh sửa văn bản phong phú (rich text editing) là một yêu cầu phổ biến trong các ứng dụng React hiện đại, nhưng hiếm khi đơn giản để triển khai. Việc cho phép người dùng định dạng văn bản với tiêu đề, danh sách và nhấn mạnh sẽ tạo ra những thách thức đáng kể về trạng thái, tính nhất quán của nội dung và khả năng bảo trì. Nhiều ứng dụng ban đầu dựa vào các chuỗi HTML thô hoặc sử dụng trực tiếp thuộc tính `contenteditable`. Mặc dù cách tiếp cận này có thể hoạt động ban đầu, nhưng thường dẫn đến hành vi không thể đoán trước khi ứng dụng phát triển. Nội dung trở nên khó xác thực, khó quản lý phiên bản và dễ bị hỏng khi có nhiều người dùng hoặc hệ thống tự động tham gia. Một cách tiếp cận đáng tin cậy hơn là coi văn bản phong phú như dữ liệu có cấu trúc thay vì chỉ là mã đánh dấu. Các mô hình nội dung rõ ràng giúp ứng dụng kiểm soát việc tạo, hiển thị và mở rộng nội dung, đồng thời phù hợp tự nhiên với mô hình khai báo, điều khiển trạng thái của React. Bài viết này sẽ khám phá lý do tại sao rich text editing lại khó trong React, cách nội dung có cấu trúc giúp giải quyết những thách thức này, và cách các trình soạn thảo dựa trên tiện ích mở rộng cho phép tùy chỉnh an toàn. Chúng ta cũng sẽ đi sâu vào ví dụ từng bước sử dụng trình soạn thảo văn bản phong phú của Puck để minh họa các khái niệm này trong thực tế.

Nhu Cầu và Thách Thức của Rich Text Editing trong React

Nhu cầu về rich text editor trong các ứng dụng web ngày càng tăng cao, từ các hệ thống quản lý nội dung (CMS) đến các ứng dụng mạng xã hội, cho phép người dùng tạo ra nội dung đa dạng và hấp dẫn. Tuy nhiên, việc tích hợp một rich text editor vào ứng dụng React lại tiềm ẩn nhiều phức tạp.

Thách Thức Với HTML Thô Và `contenteditable`

Việc sử dụng trực tiếp các chuỗi HTML thô hoặc thuộc tính `contenteditable` của trình duyệt, dù có vẻ tiện lợi ban đầu, thường gây ra những vấn đề lớn. Nội dung được tạo ra theo cách này thường thiếu cấu trúc rõ ràng, khiến việc xác thực, chuyển đổi hoặc quản lý trở nên khó khăn. Hơn nữa, hành vi của trình soạn thảo trở nên khó lường và dễ bị lỗi, đặc biệt khi nhiều người dùng cùng chỉnh sửa hoặc khi có các hệ thống tự động hóa tham gia.

Lợi Ích Của Dữ Liệu Có Cấu Trúc

Để khắc phục những hạn chế này, cách tiếp cận hiệu quả hơn là coi văn bản phong phú như dữ liệu có cấu trúc. Thay vì chỉ lưu trữ HTML, ứng dụng sẽ làm việc với một mô hình dữ liệu rõ ràng, nơi mỗi phần tử nội dung (ví dụ: tiêu đề, đoạn văn, danh sách) được định nghĩa và quản lý theo một cấu trúc nhất quán. Điều này không chỉ giúp kiểm soát chặt chẽ hơn quá trình tạo và hiển thị nội dung mà còn tăng cường khả năng mở rộng và bảo trì của ứng dụng.

Tại Sao Việc Xây Dựng Rich Text Editor Trong React Lại Khó Khăn?

Rich text editing thường xung đột với một số nguyên tắc cốt lõi trong cách thiết kế ứng dụng React. Nhiều giải pháp truyền thống dựa vào các API cấp trình duyệt hơn là trạng thái React rõ ràng, điều này khiến chúng khó kiểm soát hơn khi độ phức tạp tăng lên.

  • Phụ thuộc vào `contentEditable` và các thay đổi HTML do trình duyệt quản lý: Nhiều triển khai phụ thuộc vào `contentEditable` và các thay đổi DOM HTML do trình duyệt quản lý. Những hoạt động này nằm ngoài mô hình trạng thái của React và làm cho hành vi của trình soạn thảo khó lý giải.
  • Giới hạn trong việc xác thực và chuyển đổi nội dung: Việc hiển thị HTML do người dùng tạo mà không có mô hình nội dung rõ ràng sẽ giới hạn khả năng của ứng dụng trong việc xác thực, chuyển đổi hoặc lý giải nội dung một cách đáng tin cậy theo thời gian. Điều này khiến việc so sánh, quản lý phiên bản hoặc kiểm toán trở nên khó khăn, đặc biệt trong môi trường cộng tác hoặc thường xuyên chỉnh sửa.
  • Rủi ro về bố cục và quy tắc thiết kế: Những thay đổi nhỏ trong mã đánh dấu có thể dễ dàng làm hỏng bố cục hoặc vi phạm các quy tắc thiết kế và khả năng tiếp cận mà không cung cấp bất kỳ tín hiệu rõ ràng nào cho ứng dụng.
  • HTML thô thiếu ý định: Bản thân HTML thô không truyền tải được ý định, điều này khiến nó không an toàn cho các chuyển đổi tự động, chỉnh sửa theo chương trình hoặc các quy trình làm việc do AI điều khiển nếu không có một lớp cấu trúc và kiểm soát bổ sung.

Trong thực tế, nhiều trình soạn thảo hiển thị HTML như một giao diện trong khi quản lý trạng thái có cấu trúc bên trong. Thách thức cốt lõi không phải là sự hiện diện của HTML mà là liệu các ứng dụng có giữ được quyền kiểm soát và ý định về cách HTML đó được tạo ra và phát triển hay không.

Lý Do Nên Chọn Puck cho Rich Text Editing


Giao diện editor của Puck
Puck là một trình soạn thảo trực quan mã nguồn mở dành cho React, cho phép các nhóm xây dựng các trình tạo trang tùy chỉnh bằng cách sử dụng các component của riêng họ. Cốt lõi của Puck chỉ là một trình soạn thảo và một trình kết xuất React, giúp dễ dàng tích hợp vào bất kỳ ứng dụng React nào và định nghĩa hành vi chỉnh sửa thông qua cấu hình.

  • Trường dựa trên Schema (Schema-driven fields): Văn bản phong phú trong Puck được định nghĩa thông qua các schema trường rõ ràng, làm cho cấu trúc và hành vi được phép rõ ràng và có thể thực thi ở cấp ứng dụng.
  • Xây dựng trên TipTap: Trình soạn thảo văn bản phong phú của Puck được cung cấp bởi TipTap, một công cụ trình soạn thảo được áp dụng rộng rãi nổi tiếng với kiến trúc có thể mở rộng và điều khiển trạng thái.
  • Khả năng tùy chỉnh: Các tùy chọn định dạng, cấp độ tiêu đề và khả năng của trình soạn thảo có thể được bật hoặc tắt một cách linh hoạt.
  • Khả năng mở rộng theo thiết kế: Các tính năng và hành vi định dạng mới có thể được thêm vào bằng cách sử dụng tiện ích mở rộng, mà không cần sửa đổi hoặc phân nhánh lõi trình soạn thảo.
  • Phù hợp tự nhiên với React: Mô hình cấu hình của Puck phù hợp với cách tiếp cận dựa trên component của React, giúp việc chỉnh sửa văn bản phong phú dễ hiểu và dễ bảo trì hơn.

Hướng Dẫn Chi Tiết: Xây Dựng Rich Text Editor Với Puck

Một bản demo hoàn chỉnh đang hoạt động có sẵn tại kho lưu trữ GitHub này. Bạn nên sao chép kho lưu trữ và chạy dự án cục bộ để khám phá việc triển khai đầy đủ, hoặc làm theo hướng dẫn dưới đây để thiết lập từ đầu.

Để chạy bản demo cục bộ, hãy sử dụng các lệnh sau:

git clone https://github.com/Studio1HQ/puck_demo_richtext.git
cd puck_demo_richtext
npm install
npm run dev

Sau đó điều hướng đến http://localhost:3000/edit và thử nghiệm với rich text editor.

1. Cài Đặt Puck

Đầu tiên, hãy cài đặt Puck. Nếu bạn đang thêm nó vào một dự án hiện có, hãy cài đặt trực tiếp qua npm:

npm install @puckeditor/core

Ngoài ra, bạn có thể tạo một dự án mới bằng cách sử dụng công thức chính thức. Điều này thiết lập một ứng dụng React và Next.js đang hoạt động với editor và pipeline rendering đã được cấu hình sẵn.

npx create-puck-app rich-text-demo
cd rich-text-demo
npm run dev

Khi máy chủ phát triển đang chạy, điều hướng đến http://localhost:3000/edit để chỉnh sửa một trang và http://localhost:3000/ để xem nó.

Giao diện localhost của Puck
Công thức Next.js Puck bao gồm các thành phần sau:

  • Editor: Một giao diện trình soạn thảo trực quan nơi các khối nội dung có thể được thêm, cấu hình và chỉnh sửa.
  • Renderer: Một lớp kết xuất lấy dữ liệu trình soạn thảo đã lưu và xuất ra một trang trực tiếp sử dụng cùng cấu hình component.
  • Thiết lập dựa trên cấu hình: Tất cả hành vi của trình soạn thảo, các khối có sẵn và các loại trường được định nghĩa thông qua một tệp cấu hình duy nhất, thay vì logic trình soạn thảo tùy chỉnh.

Đối với bài viết này, chúng ta chỉ tập trung vào cấu hình trình soạn thảo và cách hành vi văn bản phong phú được định nghĩa. Các chi tiết về định tuyến, lưu trữ liên tục và triển khai được giữ ở mức tối thiểu để tập trung vào các khái niệm cốt lõi.

2. Thêm Rich Text Editor vào Puck

Chỉnh sửa văn bản phong phú trong Puck được định nghĩa một cách khai báo thông qua cấu hình. Thay vì tự động nối dây một trình soạn thảo, xử lý các cập nhật DOM hoặc quản lý trạng thái lựa chọn, hành vi văn bản phong phú được bật bằng cách thêm một loại trường chuyên dụng vào một định nghĩa component.

Để thiết lập, hãy thay thế nội dung của `/puck.config.tsx` bằng đối tượng cấu hình Puck sau.

import type { Config } from "@puckeditor/core";

type Props = {
  HeadingBlock: { title: string };
  RichTextBlock: { content: string };
};

export const config: Config<Props> = {
  components: {
    // Simple heading block for comparison
    HeadingBlock: {
      fields: {
        title: { type: "text" },
      },
      defaultProps: {
        title: "Heading",
      },
      render: ({ title }) => (
        <div style={{ padding: 64 }}>
          <h1>{title}</h1>
        </div>
      ),
    },

    // Basic rich text editor block
    RichTextBlock: {
      label: "Rich Text",
      fields: {
        content: {
          type: "richtext",
        },
      },
      render: ({ content }) => (
        <div style={{ padding: 64, maxWidth: 700, margin: "0 auto" }}>
          {content}
        </div>
      ),
    },
  },
};

export default config;

Tệp này đóng vai trò là cơ sở cho tất cả các phần sắp tới, nơi chúng ta sẽ dần dần thêm kiểm soát và khả năng mở rộng. Để bật chỉnh sửa nội tuyến trong Puck, tùy chọn `contentEditable` được thêm vào định nghĩa trường văn bản phong phú.

Thêm dòng sau vào trường `content` của `RichTextBlock` trong `puck.config.tsx`:

RichTextBlock: {
  fields: {
    content: {
      type: "richtext",
      contentEditable: true,
    },
  },
  render: ({ content }) => (
    <div style={{ padding: 64, maxWidth: 700, margin: "0 auto" }}>
      {content}
    </div>
  ),
},


Giao diện editor của Puck với chỉnh sửa nội tuyến
Chỉnh sửa nội tuyến hoạt động tốt cho các cập nhật nội dung đơn giản, điều chỉnh nhanh chóng và các quy trình biên tập nơi người dùng được hưởng lợi từ việc chỉnh sửa nội dung trong ngữ cảnh hình ảnh cuối cùng của nó.

3. Tùy Chỉnh Hành Vi Editor (Kiểm Soát)

Các rich text editor thường được kỳ vọng sẽ mang lại cho người dùng sự tự do hoàn toàn, nhưng trong hầu hết các ứng dụng thực tế, điều này dẫn đến nội dung không nhất quán và hệ thống thiết kế bị hỏng. Các nhóm thường cần hạn chế các tùy chọn định dạng để đảm bảo tính nhất quán về mặt hình ảnh, khả năng tiếp cận và khả năng hiển thị dự đoán được trên toàn bộ ứng dụng. Puck cho phép bạn chọn lọc bật hoặc tắt các tùy chọn định dạng dựa trên nhu cầu của sản phẩm hoặc nhóm.

Tắt Một Tùy Chọn Định Dạng (Ví Dụ In Đậm)

Ví dụ dưới đây cho thấy cách tắt tùy chọn định dạng in đậm trong trường rich text.

Thêm cấu hình `options` sau vào trường `content` của `RichTextBlock` trong `puck.config.tsx`:

RichTextBlock: {
  fields: {
    content: {
      type: "richtext",
      options: {
        bold: false,
      },
    },
  },
  render: ({ content }) => (
    <div style={{ padding: 64, maxWidth: 700, margin: "0 auto" }}>
      {content}
    </div>
  ),
},

Cấu hình này loại bỏ khả năng định dạng in đậm khỏi trình soạn thảo.

Puck editor với tùy chọn in đậm bị vô hiệu hóa

Hạn Chế Cấp Độ Tiêu Đề (Cấu Trúc)

Trong rich text editing, việc cho phép các cấp độ tiêu đề không giới hạn thường dẫn đến hệ thống phân cấp nội dung không nhất quán và khả năng tiếp cận kém. Hầu hết các ứng dụng chỉ cần một tập hợp nhỏ, được xác định rõ ràng các cấp độ tiêu đề để duy trì sự rõ ràng và nhất quán về mặt hình ảnh. Puck cũng cho phép bạn giới hạn hành vi tiêu đề thông qua cấu hình, đảm bảo rằng nội dung tuân theo một cấu trúc dự đoán được.

Ví dụ dưới đây hạn chế tiêu đề chỉ ở **H1 và H2**.

Thêm cấu hình tiêu đề sau vào đối tượng `options` của trường `content` trong `puck.config.tsx`:

RichTextBlock: {
  fields: {
    content: {
      type: "richtext",
      options: {
        heading: {
          levels: [1, 2],
        },
      },
    },
  },
  render: ({ content }) => (
    <div style={{ padding: 64, maxWidth: 700, margin: "0 auto" }}>
      {content}
    </div>
  ),
},

Sau khi áp dụng, trình soạn thảo sẽ chỉ cho phép người dùng chọn H1 và H2. Tất cả các cấp độ tiêu đề khác sẽ bị xóa khỏi giao diện trình soạn thảo và không thể áp dụng thông qua phím tắt.

Puck editor chỉ hiển thị H1 và H2

Tùy Chỉnh Thanh Menu (Kiểm Soát UI)

Các thanh công cụ mặc định của trình soạn thảo được thiết kế để bao gồm nhiều trường hợp sử dụng, nhưng trong các ứng dụng thực tế, chúng thường hiển thị nhiều tùy chọn hơn mức cần thiết. Điều này có thể gây choáng ngợp cho người dùng và dẫn đến nội dung không nhất quán. Hầu hết các nhóm đều được hưởng lợi từ việc trình bày một thanh công cụ được đơn giản hóa chỉ phản ánh các tùy chọn định dạng mà họ muốn hỗ trợ. Puck cho phép bạn tùy chỉnh thanh menu rich text và quyết định các điều khiển và component nào sẽ được hiển thị, giúp kiểm soát trải nghiệm người dùng trình soạn thảo mà không cần sửa đổi công cụ trình soạn thảo hoặc viết mã UI tùy chỉnh.

Ví dụ dưới đây cho thấy cách định nghĩa một thanh menu tùy chỉnh chỉ hiển thị các điều khiển được chọn.

Thêm hàm `renderMenu` vào trường `content` của `RichTextBlock` trong `puck.config.tsx`:

import { RichTextMenu } from "@puckeditor/core";

content: {
  type: "richtext",
  renderMenu: () => (
    <RichTextMenu>
      <RichTextMenu.Group>
        <RichTextMenu.Bold />
      </RichTextMenu.Group>
    </RichTextMenu>
  ),
},

Cấu hình này thay thế thanh công cụ mặc định bằng một menu tối thiểu chỉ bao gồm điều khiển in đậm. Tất cả các tùy chọn định dạng khác đều bị ẩn khỏi giao diện.

Puck editor với thanh công cụ tùy chỉnh

4. Thêm Tiện Ích Mở Rộng Tùy Chỉnh (Khả Năng Mở Rộng)

Các rich text editor hiện đại không phải là các hệ thống nguyên khối. Thay vì tích hợp mọi tính năng vào lõi trình soạn thảo, chức năng nâng cao được thêm vào bằng cách mở rộng chính công cụ trình soạn thảo. Cách tiếp cận này giúp trình soạn thảo nhẹ trong khi cho phép các ứng dụng giới thiệu các khả năng mới chỉ khi cần thiết. Rich text editor của Puck tuân theo mô hình này. Nó được xây dựng trên TipTap, cho phép thêm các hành vi định dạng mới bằng cách đăng ký các tiện ích mở rộng thông qua cấu hình.

Ví dụ dưới đây thêm hỗ trợ định dạng superscript bằng cách đăng ký tiện ích mở rộng Superscript.

Đầu tiên, hãy cài đặt tiện ích mở rộng Superscript:

npm install @tiptap/extension-superscript

Sau đó, thêm tiện ích mở rộng vào cấu hình trường rich text trong `puck.config.tsx`:

import Superscript from "@tiptap/extension-superscript";

content: {
  type: "richtext",
  tiptap: {
    extensions: [Superscript],
  },
},

Cấu hình này cho phép chức năng superscript ở cấp độ công cụ trình soạn thảo. Trình soạn thảo hiện đã hiểu định dạng superscript và có thể áp dụng nó nội bộ, nhưng người dùng chưa thấy nút hoặc điều khiển thanh công cụ để kích hoạt nó. Tách biệt khả năng của trình soạn thảo với giao diện người dùng của trình soạn thảo là một nguyên tắc thiết kế quan trọng. Bằng cách thêm chức năng thông qua các tiện ích mở rộng trước, các ứng dụng có thể kiểm soát thời điểm và cách các tính năng được hiển thị cho người dùng. Điều này giúp trình soạn thảo dễ phát triển hơn, an toàn hơn khi tùy chỉnh và phù hợp hơn với các yêu cầu của sản phẩm. Trong bước tiếp theo, khả năng này sẽ được hiển thị thông qua một điều khiển thanh công cụ tùy chỉnh.

Hiện Thị Tiện Ích Mở Rộng Qua Điều Khiển Tùy Chỉnh

Để làm cho tính năng có thể sử dụng được, trình soạn thảo cần hiển thị khả năng này thông qua giao diện người dùng. Điều này được thực hiện bằng cách kết nối trạng thái trình soạn thảo với thanh công cụ bằng cách sử dụng một selector và hiển thị một điều khiển tùy chỉnh phản hồi trạng thái đó. Một selector cho phép trình soạn thảo hiển thị thông tin về trạng thái hiện tại của nó, chẳng hạn như liệu một tùy chọn định dạng có đang hoạt động hay không hoặc liệu nó có thể được áp dụng tại một vị trí con trỏ nhất định hay không. Trạng thái này sau đó được thanh menu sử dụng để kiểm soát hành vi của nút.

Ví dụ dưới đây cho thấy cách hiển thị tiện ích mở rộng Superscript thông qua một nút thanh công cụ tùy chỉnh.

Cập nhật trường `content` trong `puck.config.tsx` như sau:

// Import icon cho Superscript. Bạn cần tạo component này.
// Ví dụ đơn giản:
const SuperscriptIcon = ({ size }) => (
  <svg width={size} height={size} viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
    <path d="M10 5.5H14C14.8284 5.5 15.4284 5.5 15.892 5.9636C16.3556 6.4272 16.5 7.0272 16.5 7.5V8.5C16.5 9.17157 16.5 9.67157 16.5 10C16.5 10.3284 16.5 10.5 16.5 10.5C16.5 10.5 16.5 10.5 16.5 10.5V10.5M8 12.5H16" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
    <path d="M12 16.5V20.5" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
  </svg>
);

// ... (các import và cấu hình khác)

// Rich text block demonstrating structure, control, extensibility
RichTextBlock: {
  label: "Rich Text",
  fields: {
    content: {
      type: "richtext",

      // Thêm TipTap extension tùy chỉnh
      tiptap: {
        extensions: [Superscript],
        selector: ({ editor }) => ({
          isSuperscript: editor?.isActive("superscript"),
          canSuperscript: editor
            ?.can()
            .chain()
            .toggleSuperscript()
            .run(),
        }),
      },

      // Thanh công cụ tùy chỉnh với điều khiển Superscript
      renderMenu: ({ children, editor, editorState }) => (
        <RichTextMenu>
          {children}

          <RichTextMenu.Group>
            <RichTextMenu.Control
              title="Superscript"
              icon={<SuperscriptIcon size={16} />}
              onClick={() =>
                editor?.chain().focus().toggleSuperscript().run()
              }
              active={editorState?.isSuperscript}
              disabled={!editorState?.canSuperscript}
            />
          </RichTextMenu.Group>
        </RichTextMenu>
      ),
    },
  },
  render: ({ content }) => (
    <div style={{ padding: 64, maxWidth: 700, margin: "0 auto" }}>
      {content}
    </div>
  ),
},


Puck editor với điều khiển Superscript tùy chỉnh
Cấu hình này kết nối tiện ích mở rộng Superscript với giao diện trình soạn thảo. Selector hiển thị liệu superscript có đang hoạt động hay không và liệu nó có thể được áp dụng hay không, và điều khiển tùy chỉnh sử dụng trạng thái đó để hiển thị một nút thanh công cụ tương tác.

Khám Phá Thêm

Bạn có thể mở rộng bản demo này bằng cách:

Những Điểm Chính Cần Nhớ

Chỉnh sửa văn bản phong phú hoạt động tốt nhất khi nó được coi là một hệ thống có cấu trúc thay vì một dạng nhập liệu tự do. Bằng cách mô hình hóa nội dung một cách có chủ đích, thực thi các quy tắc rõ ràng và mở rộng hành vi thông qua cấu hình, các nhóm có thể xây dựng các trình soạn thảo có khả năng mở rộng đáng tin cậy với các ứng dụng của họ. Puck phù hợp tự nhiên với mô hình này bằng cách kết hợp một công cụ văn bản phong phú có cấu trúc với cách tiếp cận dựa trên cấu hình phù hợp với phát triển React hiện đại.

  • Cấu trúc thay vì mã đánh dấu không kiểm soát: Văn bản phong phú nên mã hóa ý định và hệ thống phân cấp thông qua cấu trúc rõ ràng, không chỉ dựa vào HTML do trình duyệt quản lý.
  • Kiểm soát thay vì chỉnh sửa tự do: Các trình soạn thảo nên thực thi các quy tắc thiết kế và nội dung thay vì dựa vào việc dọn dẹp thủ công.
  • Khả năng mở rộng thông qua cấu hình: Các khả năng mới có thể được thêm vào một cách an toàn mà không cần sửa đổi logic cốt lõi của trình soạn thảo.
  • Tách biệt các mối quan tâm: Hành vi chỉnh sửa, giao diện người dùng và kết xuất vẫn được tách biệt rõ ràng.

Khám phá kho lưu trữ demo đầy đủ để xem các khái niệm này trong thực tế, thử nghiệm với các ràng buộc của trình soạn thảo và mở rộng rich text editor để phù hợp với các trường hợp sử dụng của riêng bạn.

Chỉ mục