IlmHamroh
JavaScript Full-stack/21-qism. React: media, integratsiyalar, PWA11/32-dars19 daqiqa
Mundarija (30)

Lexical, BlockNote, Monaco va CodeMirror React'da: blok muharrirlar, kod muharrirlari va hamkorlikda tahrir

Qisqacha: Muharrir tanlash — vazifadan. Matn uchun Lexical (Meta, yengil, +71.4 KB, CSP 0) yoki Tiptap. Notion uslubidagi bloklar uchun BlockNote (tayyor, lekin +373.1 KB va Trusted Types'da yiqildi). O'z muharrir freymvorki kerak bo'lsa — Slate/Plate. Kod uchun Monaco — VS Code'ning o'zi, standart holda o'zini jsDelivr CDN'dan yuklaydi. Yoki CodeMirror 6 — modulli (+165.8 KB), lekin o'z <style> teglarini qo'shadi. Bir hujjatni ko'p odam birga tahrirlashi — Yjs (CRDT) bilan, ko'pchilik muharrirda tayyor ulagich bor. vazifalar da hech biri yo'q: bitta izoh uchun <textarea> yetarli.

Bu darsda

  • Lexical'ning holat modelini (update, read, tinglovchilar) tushunasiz va uni Tiptap bilan solishtirasiz.
  • BlockNote va Plate/Slate qachon to'g'ri tanlov ekanini bilasiz.
  • Monaco va CodeMirror'ni React'ga ulaysiz, CDN va CSP muammolarini hal qilishni bilasiz.
  • Kod muharririda klaviatura tuzog'i (Tab) va a11y'ni hisobga olasiz.
  • Hamkorlikda tahrir (Yjs, CRDT) g'oyasini kichik modelda ko'rasiz.

Oldin bilishingiz kerak: Rich text muharrir I: Tiptap, Kod bloklari va sintaksis yoritish, Brauzer xavfsizlik modeli, Web Workers, Fokus boshqaruvi va klaviatura navigatsiyasi.

1. Nega bu kerak?

O'tgan darsda Tiptap bilan menyu ta'rifi muharririni ko'rdik. Endi «Bahor»da uch xil yangi ehtiyoj bor.

Birinchisi — bilimlar bazasi. Jasur aka oshxona retseptlarini Notion'dagidek saqlamoqchi: har qism — blok, / bosilsa menyu chiqadi, bloklarni sudrab tartiblash mumkin.

Ikkinchisi — kod. Sardor ichki admin panelga JSON sozlamalarni tahrirlaydigan sahifa yasayapti. Unga qator raqamlari, xatolarni qizil chiziq bilan ko'rsatish, avto-to'ldirish kerak — xuddi VS Code'dagidek.

Uchinchisi — birga tahrir. Ikki menejer bir vaqtda bitta retseptni o'zgartiradi. Kim oxirgi saqlasa, o'shaniki qoladimi?

Bitta muharrir hammasiga mos emas. Hayotiy o'xshatish — oshxona pichoqlari. Non kesadigan, go'sht chopadigan, sabzavot tozalaydigan — hammasi pichoq, lekin oshpaz har ishga o'zinikini oladi. Muharrirlar ham shunday: matn, bloklar, kod — har biriga o'z asbobi.

2. Lexical: Meta'ning muharrir freymvorki

2.1 G'oya: o'zgarmas holat va update

Lexical — Meta (Facebook) ochiq kod sifatida chiqargan muharrir freymvorki. Tiptap kabi u ham DOM'ni haqiqat manbai deb bilmaydi. Hujjat — editor state, u faqat editor.update(() => { … }) ichida o'zgaradi. Tashqarida — faqat o'qish: editorState.read(() => { … }). Har o'zgarishdan keyin tinglovchilar xabar oladi.

Bu g'oyani kutubxonasiz kichik modelda ko'raylik. Bu Lexical'ning o'zi emas — faqat uning qoidasi:

js
"use strict";
function createEditor(initial) {
  let state = Object.freeze(structuredClone(initial));
  const listeners = new Set();
  return {
    getEditorState: () => state,
    update(fn) {
      const draft = structuredClone(state); // qoralama nusxa
      fn(draft);
      state = Object.freeze(draft); // yangi, o'zgarmas holat
      for (const l of listeners) l(state);
    },
    registerUpdateListener(l) {
      listeners.add(l);
      return () => listeners.delete(l); // obunani bekor qilish
    },
  };
}

const editor = createEditor({ text: "Osh" });
const off = editor.registerUpdateListener((s) =>
  console.log("yangi:", s.text),
);

editor.update((draft) => {
  draft.text += " — 35 000";
});

try {
  editor.getEditorState().text = "buzdim";
} catch (e) {
  console.log(e.constructor.name, "— update'siz o'zgarmaydi");
}
off();
editor.update((draft) => {
  draft.text = "Manti";
});
console.log("oxirgi:", editor.getEditorState().text);

Konsolda:

text
yangi: Osh — 35 000
TypeError — update'siz o'zgarmaydi
oxirgi: Manti

Uchta qoida ko'rindi. O'zgarish — faqat update ichida, qoralama nusxada. Tashqarida holat muzlatilgan — yozishga urinish TypeError (Object.freeze). registerUpdateListener bekor qilish funksiyasini qaytaradi — off() dan keyin tinglovchi chaqirilmadi. Bu React effektidagi cleanup bilan juda mos: useEffect(() => editor.registerUpdateListener(…), [editor]).

2.2 Haqiqiy Lexical: holat JSON'i va $ funksiyalar

Lexical'da update ichida maxsus $ funksiyalar ishlaydi: $getRoot(), $createParagraphNode(), $createTextNode(). Ular "hozir faol holat" ga murojaat qiladi. Biz Lexical 0.52.0 ni Node 24 da, @lexical/headless (brauzersiz muharrir) bilan ishga tushirdik:

ts
import { createHeadlessEditor } from "@lexical/headless";
import {
  $getRoot,
  $createParagraphNode,
  $createTextNode,
} from "lexical";

const editor = createHeadlessEditor({
  namespace: "menyu",
  onError: (e) => {
    throw e;
  },
});
editor.update(
  () => {
    const p = $createParagraphNode();
    p.append(
      $createTextNode("Osh — "),
      $createTextNode("35 000").toggleFormat("bold"),
    );
    $getRoot().append(p);
  },
  { discrete: true }, // darhol, sinxron bajarilsin
);
console.log(JSON.stringify(editor.getEditorState().toJSON()));

Natija (qisqartirilgan). Ikki text tugunidagi "format" qiymatiga qarang — birida 0, ikkinchisida 1:

text
{"root":{"children":[{"children":[{"format":0,"text":"Osh — ","type":"text",…},{"format":1,"text":"35 000","type":"text",…}],"type":"paragraph",…}],"type":"root",…}}

Tiptap bilan farqqa qarang: qalin — marks: [{ type: "bold" }] emas, "format": 1. Lexical formatlarni bit maska sifatida saqlaydi: 1 — qalin, 2 — kursiv, 3 — ikkalasi (bitli operatorlar). JSON ixcham, lekin o'qish qiyinroq.

$getRoot() ni update dan tashqarida chaqirsak, Lexical aniq xato beradi (o'sha sinovda olindi):

text
Unable to find an active editor state. State helpers or node methods can only be used synchronously during the callback of editor.update(), editor.read(), or editorState.read().

Tarjimasi: "Faol muharrir holati topilmadi. Holat yordamchilari faqat editor.update(), editor.read() yoki editorState.read() ichida, sinxron ishlatiladi". "Sinxron" so'ziga e'tibor bering: update ichida await dan keyin $ funksiya ham shu xatoni beradi.

2.3 React'da: kompozitor va plaginlar

React tomonda Lexical — komponentlar to'plami (kanon tajribasidagi kod):

tsx
import { LexicalComposer as Composer } from
  "@lexical/react/LexicalComposer";
import { RichTextPlugin } from "@lexical/react/LexicalRichTextPlugin";
import { ContentEditable as Editable } from
  "@lexical/react/LexicalContentEditable";
import { LexicalErrorBoundary as Boundary } from
  "@lexical/react/LexicalErrorBoundary";
import { HistoryPlugin } from "@lexical/react/LexicalHistoryPlugin";
import { OnChangePlugin } from "@lexical/react/LexicalOnChangePlugin";

<Composer
  initialConfig={{ namespace: "menyu", onError: (e) => { throw e; } }}
>
  <RichTextPlugin
    contentEditable={<Editable aria-label="Retsept" />}
    ErrorBoundary={Boundary}
  />
  <HistoryPlugin />
  <OnChangePlugin
    onChange={(state) => save(JSON.stringify(state.toJSON()))}
  />
</Composer>;

Imkoniyatlar — plaginlar (React komponentlari): tarix, ro'yxat, havola, jadval. Toolbar — o'zingiz yozasiz, useLexicalComposerContext() bilan muharrir nusxasini olib. Tiptap'dagi tayyor StarterKit yo'q — erkinlik ko'p, tayyor narsa kam.

Kanon o'lchovi: Lexical 0.52 — +71.4 KB, CSP va Trusted Types bilan 0 buzilish (bo'sh muharrir, 4 element chizildi). Bu darsda o'lchangan muharrirlar ichida qattiq CSP bilan to'g'ridan-to'g'ri ishlagani — faqat Lexical.

Tekshirib ko'ring: Lexical formatida "format": 3 nima degani? Kursiv va tagiga chizilgan (underline — 8) matn uchun qiymat qancha?

Javob

3 = 1 | 2 — qalin va kursiv birga. Kursiv (2) va tagiga chizish (8): 2 | 8 = 10. Bit maska har formatga bitta bit ajratadi, shuning uchun bitta son ichida bir nechta "ha/yo'q" saqlanadi. Tekshirish — format & 2 (kursivmi?).

3. Blok muharrirlar: BlockNote, Plate va Slate

3.1 BlockNote — tayyor Notion

Blok muharrir — hujjat paragraflar oqimi emas, bloklar ro'yxati: sarlavha, ro'yxat, rasm, jadval — har biri alohida, sudrab ko'chiriladi. / bosilsa "slash menu" chiqadi. Notion, Confluence shunday ishlaydi.

BlockNote — shuning tayyor React varianti. Ichida ProseMirror va Tiptap, UI esa Mantine (yoki boshqa UI to'plami) bilan. Kanon tajribasidagi kod — atigi uch qator:

tsx
import { useCreateBlockNote } from "@blocknote/react";
import { BlockNoteView } from "@blocknote/mantine";
import "@blocknote/mantine/style.css";

function RecipeEditor() {
  const editor = useCreateBlockNote();
  return <BlockNoteView editor={editor} />;
}

Uch qatorga slash menyu, sudrash tutqichlari, formatlash paneli — hammasi tayyor. Narxi ham shunga yarasha: kanon o'lchovi +373.1 KB JS va +37.6 KB CSS (Mantine bilan). Bu React'ning beshdan ortiq barobari. Trusted Types ostida yiqildi (TrustedHTML, 0 element). Ichida Tiptap bo'lgani uchun o'tgan darsdagi sabablar ehtimol shu yerda ham — lekin biz BlockNote'ni alohida qazimadik.

Yana bir halol eslatma: BlockNote kanonda birinchi yig'ishda xato berdi va peer bog'liqliklar o'rnatilgach qayta o'lchandi (kutubxonalar-21-qisman.txt). O'rnatishda peer ogohlantirishlarini o'qing.

3.2 Slate va Plate — o'zingiz qurasiz

Slate — muharrir yasash uchun freymvork: hujjat modeli va tahrir yadrosi, qolgani sizdan. Plate — Slate ustidagi plaginlar va komponentlar to'plami (shadcn/ui uslubida ko'chiriladigan komponentlar bilan). Ular o'z muharrir mahsulotini qurayotgan jamoalar uchun. Bu ikkisini kanon o'lchamadi — xaritada tanishuv uchun.

Tanlov qoidasi qisqa:

flowchart TB
  Q{"Nima tahrirlanadi?"} -->|"oddiy izoh"| T["textarea + Markdown"]
  Q -->|"formatli matn"| R{"Tayyor to'plam kerakmi?"}
  R -->|"ha"| TT["Tiptap"]
  R -->|"yengil, o'zim quraman"| LX["Lexical"]
  Q -->|"Notion uslubi, bloklar"| BN["BlockNote"]
  Q -->|"o'z muharrir mahsuloti"| PL["Slate / Plate"]
  Q -->|"kod"| K{"VS Code darajasi?"}
  K -->|"ha"| M["Monaco"]
  K -->|"yengil, modulli"| CM["CodeMirror 6"]

Diagrammaning birinchi shoxiga qarang: eng ko'p holatda javob — kutubxonasiz <textarea>. Kanon vazifalar da aynan shuni tanladi.

4. Kod muharrirlari

4.1 Monaco — VS Code'ning o'zi

Monaco — VS Code ichidagi muharrir, alohida paket bo'lib chiqarilgan. Avto-to'ldirish, xato ko'rsatish, ko'p kursor, mini-xarita — VS Code'dagi hamma narsa. React o'rami — @monaco-editor/react (kanonda 4.7.0):

tsx
import Editor from "@monaco-editor/react";

<Editor
  height={240}
  defaultLanguage="json"
  defaultValue={'{ "narx": 35000 }'}
  options={{ minimap: { enabled: false }, fontSize: 14 }}
  onChange={(value) => setDraft(value ?? "")}
/>;

Kanon o'lchovi g'alati ko'rinadi: atigi +4.5 KB. Sabab — o'ram Monaco'ning o'zini bundle'ga qo'shmaydi. U uni ishga tushganda CDN'dan yuklaydi. Biz @monaco-editor/loader 1.7.0 manbasini ochdik — sukut manzil:

text
vs: 'https://cdn.jsdelivr.net/npm/monaco-editor@0.55.1/min/vs'

Qiziq tafsilot: kanon monaco-editor 0.57.0 ni npm'dan o'rnatgan, lekin brauzer baribir CDN'dagi 0.55.1 ni yuklaydi — o'rnatilgan paket umuman ishlatilmaydi. Sabab oddiy: loader ichiga yozib qo'yilgan manzil loader chiqqan paytdagi Monaco versiyasini ko'rsatadi. npm'da 0.56.0 va 0.57.0 chiqqanidan u bexabar.

Bu uch muammo beradi:

  1. CSP va Trusted Types — tashqi skript. Kanonda vazifalar CSP'si ostida TrustedScriptURL bilan to'sildi (0 element). CSP'siz ochilganda brauzer cdn.jsdelivr.net ga bordi.
  2. Maxfiylik va ishonch — har ochilishda uchinchi tomon serveri so'rov oladi; CDN buzilsa — sizning sahifangizda begona kod.
  3. Oflayn — internet bo'lmasa, muharrir yo'q.

Yechim — Monaco'ni o'z bundle'ingizdan berish. @monaco-editor/react hujjatidagi usul:

tsx
import { loader } from "@monaco-editor/react";
import * as monaco from "monaco-editor";
import editorWorker from
  "monaco-editor/esm/vs/editor/editor.worker?worker";

self.MonacoEnvironment = {
  getWorker: () => new editorWorker(), // Vite: ishchi alohida fayl
};
loader.config({ monaco }); // CDN emas, npm paketdagi Monaco

Endi Monaco butunlay sizning bundle'ingizda — CDN yo'q, lekin hajm keskin oshadi (kanon bu variantni o'lchamadi; o'zingiz npm run build bilan ko'ring). Ishchilar (Web Workers) uchun CSP'da worker-src 'self', Trusted Types siyosatida esa ishchi manzili kerak bo'ladi.

4.2 CodeMirror 6 — modulli va yengilroq

CodeMirror 6 — boshqa yo'l: kichik yadro va kerakli qismlar (til, mavzu, avto-to'ldirish) alohida paketlar. React o'rami — @uiw/react-codemirror (kanonda 4.25.12):

tsx
import CodeMirror from "@uiw/react-codemirror";
import { javascript } from "@codemirror/lang-javascript";

<CodeMirror
  value="const osh = 35000;"
  height="200px"
  extensions={[javascript()]}
  onChange={(value) => setDraft(value)}
  aria-label="Narx skripti"
/>;

Kanon o'lchovi +165.8 KB (o'ram basicSetup ni ham qo'shadi: qator raqamlari, qavslar, tarix, qidiruv). CSP ostida natija boshqacha: muharrir chizildi (21 element, CSP'siz bilan bir xil), lekin bitta buzilish — style-src-elem. CodeMirror stillarini JS'dan, <style> tegi yaratib qo'shadi, vazifalar siyosati (style-src 'self') esa inline <style> ni to'sadi. Natija: muharrir ishlaydi, lekin stilsiz.

CodeMirror buni biladi: EditorView.cspNonce sozlamasi (biz @codemirror/view 6.43.14 tur faylida topdik). Server har javobga tasodifiy nonce yozadi (style-src 'self' 'nonce-…'), CodeMirror esa shu nonce'ni o'z <style> iga qo'yadi. Lekin statik hostingda (GitHub Pages — vazifalar shunday) har so'rovga yangi nonce yasab bo'lmaydi. Bu — CSP'ning server bilan bog'liq qismi; unga 25-qism (HTTP, Express va REST API) da qaytamiz.

4.3 Kod muharriri va klaviatura

Oddiy formada Tab — keyingi maydonga o'tish. Kod muharririda esa Tab — chekinish (indent). Agar muharrir Tab'ni har doim "ushlasa", klaviatura bilan ishlaydigan Bobur muharrirdan chiqa olmaydi — bu klaviatura tuzog'i (WCAG 2.1.2 "No Keyboard Trap").

Ikki muharrir buni turlicha hal qiladi:

  • CodeMirror 6 — Tab sukut bo'yicha chekinish emas: muharrirdan chiqadi. Chekinish kerak bo'lsa, indentWithTab ni o'zingiz ulaysiz va hujjatda tavsiya qilinganidek, chiqish yo'lini aytasiz (Esc, keyin Tab).
  • Monaco — Tab chekinish qiladi, Ctrl+M esa "Tab fokusni ko'chiradi" rejimini yoqadi.

Qaysi biri bo'lmasin, muharrir yonida bitta qator yordam bering: "Muharrirdan chiqish: Esc, keyin Tab". Va muharrirga nom bering (aria-label) — ekran o'quvchi "tahrirlanadigan matn" emas, "Narx skripti" desin.

Tekshirib ko'ring: Admin panelda kichik JSON sozlamani tahrirlash kerak. Monaco'ning CDN'dan yuklanishi, CodeMirror'ning stillari CSP'ga mos emas. Yana qanday yo'l bor?

Javob

Oddiy <textarea> + saqlashdan oldin JSON.parse bilan tekshirish va xatoni ko'rsatish. Yoritish kerak bo'lsa — o'tgan darsdagi Prism (CSP 0) bilan faqat ko'rish rejimida. Kichik sozlama uchun bu yetarli va CSP'ni kengaytirishni talab qilmaydi. Kod muharriri katta va tez-tez tahrirlanadigan kod uchun o'zini oqlaydi.

5. Hamkorlikda tahrir: Yjs va CRDT

5.1 Muammo: kim oxirgi saqlasa

Ikki menejer bir retsept ro'yxatini oflayn tahrirladi. Dilnoza opa "zira" qo'shdi, Jasur aka "sabzi" ni o'chirdi. Ikkalasi tarmoqqa qaytdi. Eng sodda yo'l — oxirgi yozuv yutadi (last write wins): kim keyin saqlasa, o'sha butun ro'yxatni almashtiradi. Biri ishi yo'qoladi.

CRDT (Conflict-free Replicated Data Type) — boshqa yo'l: o'zgarishlar shunday saqlanadiki, ularni istalgan tartibda birlashtirsa ham natija bir xil bo'ladi. Eng sodda misol — o'chirishlar "qabr toshi" (tombstone) bilan belgilanadigan to'plam:

js
// har band — noyob id bilan; o'chirish — faqat belgi
const base = [
  { id: "a1", text: "guruch", deleted: false },
  { id: "a2", text: "sabzi", deleted: false },
];

// Dilnoza: "zira" qo'shdi
const dilnoza = [...base, { id: "d1", text: "zira", deleted: false }];
// Jasur: "sabzi" ni o'chirdi
const jasur = base.map((x) =>
  x.id === "a2" ? { ...x, deleted: true } : x,
);

// 1) oxirgi yozuv yutadi: Jasur keyin saqladi
const lww = jasur;

// 2) CRDT uslubida birlashtirish: id bo'yicha, o'chirish ustun
function merge(a, b) {
  const all = new Map();
  for (const x of [...a, ...b]) {
    const old = all.get(x.id);
    const deleted = x.deleted || Boolean(old?.deleted);
    all.set(x.id, { ...x, deleted });
  }
  return [...all.values()];
}

const show = (list) =>
  list.filter((x) => !x.deleted).map((x) => x.text).join(", ");
console.log("oxirgi yutadi:", show(lww));
console.log("birlashtirish:", show(merge(dilnoza, jasur)));
console.log("teskari tartib:", show(merge(jasur, dilnoza)));

Konsolda:

text
oxirgi yutadi: guruch
birlashtirish: guruch, zira
teskari tartib: guruch, zira

"Oxirgi yutadi" — "zira" yo'qoldi. Birlashtirishda ikkala o'zgarish ham saqlandi, va tartib ahamiyatsiz: merge(a, b) va merge(b, a) bir xil. Bu — CRDT'ning asosiy xususiyati. Matn uchun CRDT ancha murakkab (har harfning noyob joyi), lekin g'oya shu.

5.2 Yjs — tayyor CRDT

Yjs — JavaScript uchun eng mashhur CRDT kutubxonasi. U hujjatni (Y.Doc) o'zgarishlar oqimi sifatida saqlaydi. Muharrirlar uchun ulagichlar bor: ProseMirror/Tiptap (y-prosemirror, Tiptap'ning Collaboration kengaytmasi), Lexical (@lexical/yjs), CodeMirror (y-codemirror.next), Monaco (y-monaco). Tarmoq — provayder: y-websocket (server orqali), y-webrtc (brauzerlar to'g'ridan-to'g'ri), y-indexeddb (oflayn saqlash).

Boshqa foydalanuvchilarning kursori va ismi — awareness (xabardorlik) qismi: "Dilnoza shu yerda yozyapti" degan rangli belgi. Bu ma'lumot hujjatga yozilmaydi, faqat jonli uzatiladi.

Kanon Yjs'ni o'lchamadi va ishga tushirmadi — u server (WebSocket) talab qiladi, backend qismlari (24-qismdan) esa hali oldinda. Bu bo'lim — tanishuv: hamkorlikda tahrir kerak bo'lsa, muharrirni tanlashda "Yjs ulagichi bormi?" savolini bering.

6. Biz sinab ko'rdik: hajm va CSP

6.1 Hajm

Muharrirlar: JS (gzip, React'dan tashqari)
  • Monaco o'rami+ Monaco CDN'dan4,5 KB
  • Lexical 0.5271,4 KB
  • Tiptap 3.31.4120,4 KB
  • CodeMirror 6 (@uiw)165,8 KB
  • BlockNote 0.55+37.6 KB CSS373,1 KB

Manba: O'lchandi: kanon tajribasi, Vite 8.3.3, React 19.3.0, gzip -9, Chrome 155, natijalar/tajriba/kutubxonalar-21.txt va -qisman.txt, 2026-10-17

Monaco'ning 4.5 KB i aldamchi: haqiqiy Monaco kodi bu raqamga kirmagan — u CDN'dan keladi.

6.2 CSP va Trusted Types

Xom natija (kutubxonalar-21.txt va qayta o'lchangan BlockNote — -qisman.txt), xato matnlari qisqartirilgan:

text
muharrir-lexical     +  71.4 KB | CSP 0
muharrir-tiptap      + 120.4 KB | CSP require-trusted-types-for×2
muharrir-codemirror  + 165.8 KB | CSP style-src-elem×1
muharrir-blocknote   + 373.1 KB | CSP require-trusted-types-for×2
muharrir-monaco      +   4.5 KB | CSP require-trusted-types-for×1
  ochiq: tashqi cdn.jsdelivr.net

To'rt xil natija, to'rt xil saboq:

Muharrir CSP ostida Sabab
Lexical ishladi innerHTML va inline stil yo'q
CodeMirror ishladi, stilsiz o'z <style> tegi — nonce kerak
Tiptap, BlockNote chizilmadi HTML satr tahlili, innerHTML (Tiptap — o'tgan darsda yechim)
Monaco chizilmadi CDN skripti — TrustedScriptURL

6.3 Nega vazifalar da muharrir yo'q

Kanon izohi — <textarea> + Markdown, kanon fayli izohida aniq yozilgan: "rich text muharrir emas: #10–#11 o'lchovi — Tiptap/Lexical bitta maydon uchun ortiqcha". Eng yengili — Lexical — ham 71 KB, ya'ni hozirgi Markdown chunk'idan (46 KB) og'irroq. Kod muharriri esa vazifalar da umuman kerak emas.

7. Hujumchi nigohi

Muharrirlar — foydalanuvchi kontentining kirish eshigi. Uch xavf:

  • Saqlangan kontent. Muharrir qanday bo'lmasin, server tarmoqdan kelgan har narsani tekshiradi (o'tgan dars). Lexical JSON, Tiptap JSON, BlockNote bloklari — hammasi sxema bo'yicha validatsiya qilinadi, HTML esa tozalanadi.
  • CDN'dan kod. Monaco CDN'dan kelsa, CDN'dagi har o'zgarish sizning sahifangizda bajariladi. Ishonchli variant — o'z bundle'ingiz yoki hech bo'lmaganda aniq versiya va SRI (integrity) (Brauzer xavfsizlik modeli).
  • Kod muharririda — kod. Admin paneldagi "skript" maydoni kodni saqlaydi, lekin uni brauzerda eval qilmang. Bajariladigan kod — alohida, cheklangan muhitda (sandbox, server) va huquqlar bilan.

8. Ko'p uchraydigan xatolar

Belgi Sabab Davosi
Unable to find an active editor state… $ funksiya update/read dan tashqarida yoki await dan keyin sinxron, update ichida
Lexical tinglovchisi ikki marta ishlaydi obuna bekor qilinmadi registerUpdateListener qaytargan funksiya — cleanup
Monaco oflayn yoki CSP'da ochilmaydi CDN'dan yuklanadi loader.config({ monaco }) + ishchilar
Monaco versiyasi package.json'dagidan boshqa o'ram CDN'dagi o'z versiyasini oladi o'z bundle'ingiz yoki paths.vs
CodeMirror stilsiz <style> CSP'da to'sildi EditorView.cspNonce + server nonce
klaviatura bilan muharrirdan chiqib bo'lmaydi Tab ushlangan CodeMirror sukutini saqlang; yordam matni
BlockNote o'rnatishda peer xatolari ko'p bog'liqlik ogohlantirishlarni o'qing, versiyalarni moslang
bitta izoh uchun 300+ KB ortiqcha asbob <textarea>

9. Mashqlar

Mashqlar kurs/mashqlar/21/11-muharrirlar/ da. Jonli yechimlarni «Ishga tushir» bilan sinang.

1-mashq (oson): bo'sh joylarni to'ldiring

  • Lexical'da holatni o'zgartiradigan yagona joy: editor.
  • Lexical formatida qalin + kursiv qiymati:
  • Monaco'ni CDN o'rniga npm paketdan berish: loader.
  • CodeMirror'ning CSP nonce sozlamasi: EditorView.
  • Tartibidan qat'i nazar bir xil birlashadigan ma'lumot turi (qisqartma):
Yechim
  • update — read faqat o'qish uchun.
  • 3 — 1 (qalin) | 2 (kursiv).
  • config — loader.config({ monaco }).
  • cspNonce — server bergan nonce <style> tegiga qo'yiladi.
  • CRDT — Conflict-free Replicated Data Type; Yjs — uning JS kutubxonasi.

2-mashq (o'rta): Lexical formatini o'qish

Lexical matn tugunining format qiymatidan formatlar ro'yxatini chiqaradigan formatsOf(n) yozing. Bitlar: 1 — qalin (bold), 2 — kursiv (italic), 4 — ustidan chizilgan (strikethrough), 8 — tagiga chizilgan (underline), 16 — kod (code). Sinov: 0, 1, 3, 10, 17.

Ishora: n & bit — bitli operatorlar; bitlar va nomlar — massivdan.

Yechim
js
const FORMATS = [
  [1, "bold"],
  [2, "italic"],
  [4, "strikethrough"],
  [8, "underline"],
  [16, "code"],
];

function formatsOf(n) {
  const on = FORMATS.filter(([bit]) => (n & bit) !== 0);
  return on.map(([, name]) => name);
}

for (const n of [0, 1, 3, 10, 17]) {
  console.log(n, formatsOf(n));
}

Konsolda:

text
0 []
1 [ 'bold' ]
3 [ 'bold', 'italic' ]
10 [ 'italic', 'underline' ]
17 [ 'bold', 'code' ]

10 — ikkilik 01010: 2 va 8 bitlari yoqilgan. & faqat ikkala sonda ham 1 bo'lgan bitlarni qoldiradi, shuning uchun (n & bit) !== 0 — "shu bit yoqilganmi?". Lexical'ning o'zida bu textNode.hasFormat("bold").

3-mashq (qiyin): ikki tomonlama birlashtirish

«Muammo: kim oxirgi saqlasa» bo'limidagi merge ni uch foydalanuvchi uchun sinang: Dilnoza "zira" qo'shdi, Jasur "sabzi" ni o'chirdi, Sardor "sabzi" ni "sariq sabzi" ga o'zgartirdi (text almashdi, id o'sha). O'zgartirish uchun qoida: har bandda version (son) bor, katta version ning matni yutadi; o'chirish baribir ustun. Uch nusxani istalgan tartibda birlashtirish bir xil natija bersin.

Ishora: merge da text ni version bo'yicha tanlang; tartibni tekshirish uchun barcha 6 almashinuvni sinash shart emas — 2–3 tasi yetarli.

Yechim
js
const base = [
  { id: "a1", text: "guruch", version: 1, deleted: false },
  { id: "a2", text: "sabzi", version: 1, deleted: false },
];
const zira = { id: "d1", text: "zira", version: 1, deleted: false };
const dilnoza = [...base, zira];
const jasur = base.map((x) =>
  x.id === "a2" ? { ...x, deleted: true } : x,
);
const sardor = base.map((x) =>
  x.id === "a2" ? { ...x, text: "sariq sabzi", version: 2 } : x,
);

function merge(...lists) {
  const all = new Map();
  for (const x of lists.flat()) {
    const old = all.get(x.id);
    if (!old) {
      all.set(x.id, x);
      continue;
    }
    const newer = x.version > old.version ? x : old;
    all.set(x.id, { ...newer, deleted: x.deleted || old.deleted });
  }
  const byId = (p, q) => p.id.localeCompare(q.id);
  return [...all.values()].toSorted(byId);
}

const show = (list) =>
  list
    .map((x) => `${x.text}${x.deleted ? " (o'chgan)" : ""}`)
    .join(", ");
console.log(show(merge(dilnoza, jasur, sardor)));
console.log(show(merge(sardor, dilnoza, jasur)));
console.log(show(merge(jasur, sardor, dilnoza)));

Konsolda:

text
guruch, sariq sabzi (o'chgan), zira
guruch, sariq sabzi (o'chgan), zira
guruch, sariq sabzi (o'chgan), zira

Uch tartib — bitta natija. "sabzi" bandi o'chgan, lekin uning oxirgi matni ("sariq sabzi") ham saqlangan — o'chirish "qabr toshi" bilan. Agar keyin kimdir o'chirishni bekor qilsa, to'g'ri matn qaytadi. Oxirgi toSorted — Map qo'shilish tartibini emas, id tartibini qaytarish uchun: busiz natija birlashtirish tartibiga bog'liq bo'lardi. Haqiqiy CRDT'larda versiya o'rniga mantiqiy soatlar ishlatiladi.

4-mashq: tajriba qadami (ixtiyoriy)

vazifalar ga tegmang. Alohida Vite loyihada (kurs/tajriba/muharrirlar/) uchta sahifa yasang: Lexical (toolbar bilan), CodeMirror (JSON tili), Monaco (loader.config({ monaco }) bilan). Har biriga vazifalar CSP <meta> ini qo'shib, konsol va npm run build hajmini yozing.

Yechim

Tekshiruv ro'yxati:

  • Lexical: konsol toza (kanon — CSP 0); OnChangePlugin JSON'ida "format": 1 — qalin.
  • CodeMirror: muharrir bor, lekin stilsiz; konsolda style-src buzilishi.
  • Monaco CDN bilan: TrustedScriptURL va cdn.jsdelivr.net; loader.config({ monaco }) bilan — CDN so'rovi yo'q, build hajmi esa keskin katta (o'zingiz o'lchagan raqamni yozing).
  • Klaviatura: har muharrirdan Tab bilan chiqib ko'ring.

10. Real ishda

  • Bilimlar bazasi va hujjatlar — Notion uslubidagi mahsulotlar: blok muharrirlar, ko'pincha Yjs bilan hamkorlik.
  • Ijtimoiy tarmoqlar va chatlar — yengil rich text (Lexical, Tiptap): eslatma (@mention), havola, emoji.
  • Onlayn IDE va kurs platformalari — kod muharrirlari: Monaco (VS Code tajribasi) yoki CodeMirror (yengil, mobil). Kod "ishga tushirish" — alohida, cheklangan muhitda.
  • Admin panellar — JSON/YAML sozlama muharrirlari, ko'pincha CodeMirror.
  • Intervyu savollari: "Monaco va CodeMirror farqi?", "muharrirni CDN'dan yuklashning xavfi?", "hamkorlikda tahrirda konflikt qanday hal qilinadi?", "kod muharririda klaviatura tuzog'ini qanday oldini olasiz?".

Xulosa

  • Lexical — o'zgarmas holat, o'zgarish faqat update ichida, $ funksiyalar sinxron; formatlar — bit maska; +71.4 KB, CSP 0.
  • BlockNote — tayyor Notion uslubi (+373.1 KB, +37.6 CSS, TT'da yiqildi); Slate/Plate — o'z muharriringizni qurish uchun.
  • Monaco — VS Code muharriri; o'ram +4.5 KB, Monaco esa CDN'dan (0.55.1) — CSP, maxfiylik, oflayn muammo; yechim — loader.config({ monaco }).
  • CodeMirror 6 — modulli (+165.8 KB), o'z <style> i: CSP'da stilsiz, nonce kerak; Tab sukut bo'yicha muharrirdan chiqaradi — klaviatura tuzog'i yo'q.
  • Hamkorlik — CRDT (Yjs): birlashtirish tartibga bog'liq emas; muharrirlarning Yjs ulagichlari bor, server kerak.
  • vazifalar — <textarea> + Markdown: eng yengil muharrir ham bitta izoh uchun og'ir.

Keyingi dars: Rasm UI komponentlari: lightbox, zoom, crop va CDN transformatsiyalari — vazifalar ga profil rasmi: o'z kesish komponentimiz, canvas'dan WebP va nega react-easy-crop CSP'da ochilmadi.

Manbalar

  • Lexical: "Introduction", "Editor State", "Headless", "React" (LexicalComposer, plaginlar) — lexical.dev/docs
  • BlockNote: "Quickstart" — blocknotejs.org; Slate — docs.slatejs.org; Plate — platejs.org
  • Monaco: @monaco-editor/react README ("loader-config", "use monaco-editor as an npm package"), @monaco-editor/loader 1.7.0 manbasi (config)
  • CodeMirror 6: "System Guide", "Reference: EditorView.cspNonce", "Example: Tab Handling" — codemirror.net
  • Yjs: "Introduction", "Editor bindings", "Providers", "Awareness" — docs.yjs.dev
  • WCAG 2.2: 2.1.2 No Keyboard Trap
  • Kanon: natijalar/tajriba/kutubxonalar-21.txt, kutubxonalar-21-qisman.txt, tajriba/kutubxonalar/src/k/muharrir-*.tsx; vazifalar holat 08 VazifaIzohi.tsx izohi (2026-10-17)
Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!