- Bitta yuklab olish tugmasi uchun butun paketni yuklash muammosini qanday hal qildik -
1. Kirish
Bu chorakda ichki xabar almashish xizmatimiz Loopin’ning 7.2 yangilanishi uchun mas’ul edim. Talablardan biri “chatlarda qabul qilingan fayllar My Files’da ham saqlanishi kerak” edi.
Xuddi shu funksionallik allaqachon mavjud edi. Fayllarni saqlash domeni bo‘lgan vault’dagi FileDownloadItem komponenti fayl nomi va hajmini ko‘rsatardi, yuklab olish tugmasi bosilganda faylni yuklab olardi, uni foydalanuvchining Stash jildiga avtomatik ro‘yxatdan o‘tkazardi va hatto avval qabul qilingan fayllar uchun boshqa ikonka ko‘rsatardi. Bu taxminan 150 qatorli kichik komponent edi. Chatda ayni narsani qayta yaratish uchun sabab yo‘qdek ko‘ringani sababli, uni qayta ishlatishga qaror qildik.
Biroq bitta import qatorini qo‘shish pnpm-lock.yaml fayliga 828 qator qo‘shdi va chat foydalanmaydigan 1 MB hajmdagi PDF viewer worker fayli build natijasida paydo bo‘ldi. Ushbu maqolada bitta 150 qatorli komponentdan foydalanish uchungina butun paketni yuklash muammosini faqat zarur qismlar import qilinadigan tarzda o‘zgartirish jarayoni hujjatlashtiriladi.
2. Asosiy ma’lumotlar
Frontend’imiz pnpm workspace va Turborepo asosidagi monorepo bo‘lib, paketlar quyidagi qatlamlarga ajratilgan.
episodes/* 실제 실행 앱 (엔트리)
└─ collages/* 앱 레이아웃과 도메인 조합 계층
└─ dramas/*-view 화면 컴포넌트 (React/MUI)
└─ dramas/*-state → dramas/*-stub
Loopin va vault alohida paketlar sifatida mos ravishda loopin-view va vault-view nomlari bilan tarqatiladi. Chatda vault’dagi komponentdan foydalanish uchun ichki registry’da e’lon qilingan @vizendjs/vault-view paketini dependency sifatida qo‘shishimiz mumkin. Dastlab yozgan barcha kodim shu edi.
import { FileDownloadItem } from '@vizendjs/vault-view';
3. Sababni aniqlash — Barrel yaratgan modul grafigi
Faqat kerakli bitta komponentni import qilganim uchun tree shaking qolgan qismlarni chiqarib tashlaydi deb o‘ylagandim, ammo unday bo‘lmadi.
Qayerdan boshlashni bilmaganim uchun avval ko‘rishim mumkin bo‘lgan narsalarni tekshirdim. pnpm-lock.yaml diff’ida yangi qo‘shilgan paketlarni ko‘zdan kechirar ekanman, pdfjs-dist va react-syntax-highlighter kabi aloqasiz paketlarni, shuningdek vault-view dist ichida pdf.worker.min.mjs nomli 1 MB hajmdagi faylni topdim. Ushbu kutubxonalarni kim import qilayotganini aniqlash uchun izni teskari yo‘nalishda kuzatdim va sabab barrel faylida ekanini bildim.
vault-view’ning ommaviy kirish nuqtasidan haqiqiy og‘ir kutubxonalargacha bo‘lgan zanjir quyidagicha edi.
src/index.ts
export * from './components'
└─ components/index.ts
├─ export * from './common'
│ ├─ CodeBlock.tsx → react-syntax-highlighter
│ ├─ AgGridCheck.tsx → ag-grid
│ └─ FileDownloadItem.tsx ← 내가 필요한 건 이것 하나
├─ export * from './stash'
│ └─ mydisk/modal/LightboxPage.tsx → pdfjs-dist, react-zoom-pan-pinch
│ └─ mydisk/MyDiskViewList.tsx → ag-grid-react
├─ export * from './minipix'
├─ export * from './policy'
├─ export * from './mydisk'
└─ export * from './cabinet'
export * from './utils'
├─ date-utils.ts → dayjs
├─ stash-utils.ts → @vizendjs/vault-stub
└─ file-utils.ts → (의존성 없음)
export * butun modulni baholash uchun nomzodga aylantirgani sababli, hatto bitta komponentni import qilish ham ushbu daraxtni to‘liq modul grafigiga kiritadi. Masalan, LightboxPage.tsx quyidagicha boshlanadi.
import { TransformComponent, TransformWrapper } from 'react-zoom-pan-pinch';
import * as pdfjsLib from 'pdfjs-dist';
import pdfjsWorker from 'pdfjs-dist/build/pdf.worker';
pdfjsLib.GlobalWorkerOptions.workerSrc = pdfjsWorker;
Oxirgi qatorda bo‘lgani kabi, modulning yuqori darajasidagi kod global sozlamalarni o‘zgartirganda, bundler modulni side effect’lariga ega deb hisoblaydi va uni xavfsiz tarzda olib tashlay olmaydi. Bundan tashqari, vault-view og‘ir kutubxonalarini peerDependencies sifatida e’lon qilgan va build vaqtida ularni external deb belgilagan. external ularning bundle’ga kiritilmasligini va faqat import statement’lari qolishini anglatadi. Shuning uchun vault-view’ning dist’i kichik ko‘rinsa ham, undan foydalanadigan ilova qolgan import statement’larini o‘z bundle’ida hal qilishi kerak. Oxir-oqibat, bu tuzilma xarajatni iste’molchi Loopin zimmasiga yukladi.
4. 1-yechim — Faqat kerakli qismlarni o‘z ichiga olgan subpath entry
Mavjud entry vault ilovasi tomonidan bevosita ishlatilgan, shuning uchun uni o‘zgartira olmasdik. Shu sababli mavjud entry’ni o‘zgarishsiz qoldirib, paketning faqat bir qismiga muhtoj bo‘lgan iste’molchilar uchun alohida ikkinchi entry yaratish usulini tanladik.
// dramas/vault-view/src/common.ts
// @vizendjs/vault-view/common — 채팅용 경량 엔트리.
// components/common/index.ts 배럴(CodeBlock·AgGridCheck 포함)을 거치지 않고
// 필요한 것만 직접 재export하여 pdfjs-dist / react-syntax-highlighter 가
// 이 청크로 유입되지 않게 한다.
export { FileDownloadItem } from './components/common/FileDownloadItem';
export type { FileDownloadItemProps } from './components/common/FileDownloadItem';
// 배럴(~/utils)이 아닌 파일 직접 경로 → dayjs/vault-stub 유입 차단
export { formatFileSize } from './utils/file-utils';
Bu yerda eng muhim narsafayl yo‘lini oxirigacha to‘g‘ridan-to‘g‘ri yozish ediAvval faqat './components/common' ni ko‘rsatdim, ammo chunk hajmi deyarli o‘zgarmadi. Tekshirganimda, bu barrel CodeBlock va AgGridCheck’ni ham export qilishi, shu sababli hatto bitta oraliq daraja orqali ham barcha qo‘shni modullar yuklanayotganini aniqladim.
FileDownloadItem’ning o‘zi nimalardan foydalanishini ham tekshirdim. Bu komponentda atigi to‘rtta import bor edi.
// dramas/vault-view/src/components/common/FileDownloadItem.tsx
import { useState } from 'react';
import styled from '@emotion/styled';
import { useClipFileDownload, useStash } from '@vizendjs/vault-state';
import { formatFileSize } from '~/utils/file-utils';
U hech qanday og‘ir kutubxonadan foydalanmas va faqat vault’ning yuklab olish hamda Stash hook’larini o‘z ichiga olgan vault-state’ga bog‘liq edi. Bu uni ajratib olish mumkinligiga yetarlicha ishonch berdi, shuning uchun ishni davom ettirdim.
Xuddi shu sababga ko‘ra, formatFileSize uchun './utils' o‘rniga './utils/file-utils' ni yozdim. utils/index.ts barrel’i date-utils (dayjs) va stash-utils (vault-stub) ni birgalikda export qiladi, holbuki menga kerak bo‘lgan file-utils.ts imports’lari yo‘q sof funksiya faylidir. Barrel orqali o‘tish aloqasiz dayjs’ni ham yuklaydi, faylga to‘g‘ridan-to‘g‘ri murojaat qilish esa boshqa hech narsani yuklamaydi. Shu bosqichda meni eng ko‘p hayratlantirgani, aynan bir xil funksiya import qilinsa ham, yo‘ldagi bir darajalik farq natijani o‘zgartirishi mumkinligi bo‘ldi.
5. 2-yechim — Build’da ikkinchi entry’ni ro‘yxatdan o‘tkazish
Faylni shunchaki yaratish uni @vizendjs/vault-view/common sifatida import qila olishimizni anglatmasdi. Entry build natijasida alohida fayl sifatida chiqarilishi ham kerak edi.
// dramas/vault-view/vite.config.mts
build: {
lib: {
entry: {
index: './src/index.ts',
common: './src/common.ts',
},
name: packageJson.name,
formats: ['es'],
fileName: (format, entryName) => `${entryName}.${format}.js`,
},
rollupOptions: {
external: [
...Object.keys(packageJson.devDependencies),
...Object.keys(packageJson.peerDependencies),
],
},
}
entry’ni string’dan object’ga o‘zgartirganimda build muvaffaqiyatsiz tugadi va muammoda ancha vaqt tiqilib qoldim. Sabab UMD edi. UMD barcha export’larni yagona global o‘zgaruvchiga biriktirishni talab qilgani uchun, bir nechta entry’ni qo‘llab-quvvatlay olmasdi: ikkita entry mavjud bo‘lganda global nomni aniqlashning iloji yo‘q edi. Barcha iste’molchi ilovalar ESM bundler’lardan foydalangani sababli, format’larni ['es'] qilib belgiladim va package.json’dagi main field’ini ham ES output’iga yo‘naltirish uchun yangiladim.
6. 3-yechim — Iste’molchilarga ushbu entry orqali kirishga ruxsat berish
Fayl build output’ida yaratilgan bo‘lsa ham, unga paket tashqarisidan @vizendjs/vault-view/common yo‘li orqali kirish mumkin bo‘lishi kerak edi. Men subpath’ni exports field’iga qo‘shdim.
{
"main": "./dist/index.es.js",
"module": "./dist/index.es.js",
"exports": {
".": {
"types": "./dist/@types/src/index.d.ts",
"import": "./dist/index.es.js",
"require": "./dist/index.es.js"
},
"./common": {
"types": "./dist/@types/src/common.d.ts",
"import": "./dist/common.es.js"
},
"./styles/all.css": "./dist/vault-view.css"
},
"typesVersions": {
"*": {
"common": ["./dist/@types/src/common.d.ts"]
}
}
}
Bu bosqichda build muvaffaqiyatli o‘tdi, ammo editor modulni topa olmasligini bildirishda davom etdi. Biroz tekshiruvdan so‘ng, exports’dagi types condition faqat moduleResolution bundler yoki undan yuqori bo‘lganda hal qilinishini bildim. Paketlarimizning ayrimlari hali ham eski resolution’dan foydalangani sababli, xato faqat shu mapping’ni typesVersions’da yana bir marta e’lon qilganimizdan keyin yo‘qoldi. Ikki field ortiqchadek ko‘rinishi mumkin, ammo ular turli iste’molchilarga xizmat qildi.
7. Tekshirish — Keraksiz hech narsa yuklanmaganini tasdiqlash
Ishni tugatgach, hammasi haqiqatan ishlayotganini tekshirish uchun dist’ni bevosita ochdim. Ikki entry turli chunk’larni ko‘rsatardi.
dramas/vault-view/dist/
├─ index.es.js 1.5 KB → index-D-3TlXGH.mjs 참조
├─ index-D-3TlXGH.mjs 3.7 MB
├─ pdf.worker.min.mjs 1.0 MB
├─ common.es.js 120 B → FileDownloadItem-B17vE2-_.mjs 참조
└─ FileDownloadItem-B17vE2-_.mjs 49 KB
common.es.js faqat to‘rt qatorni o‘z ichiga oladi.
import { F as a, f } from "./FileDownloadItem-B17vE2-_.mjs";
export {
a as FileDownloadItem,
f as formatFileSize
};
Hajmdan ko‘ra aniqroq mezon har bir chunk tashqi tomondan talab qiladigan kutubxonalar ro‘yxati edi. Avval aytib o‘tilganidek, og‘ir kutubxonalar externalized qilingan va faqat import statement’lari ko‘rinishida qolgan; bu ro‘yxat iste’molchi ilova zimmasiga tushadigan yukni ifodalardi.
[배럴 엔트리] index-D-3TlXGH.mjs 가 요구하는 것 (15개)
react, react-dom, @emotion/styled, @emotion/react, @vizendjs/accent,
@tanstack/react-query, axios, notistack, jotai, lodash, dayjs,
ag-grid-react, react-zoom-pan-pinch, pdfjs-dist, react-syntax-highlighter
[경량 엔트리] FileDownloadItem-B17vE2-_.mjs 가 요구하는 것 (5개)
react, @emotion/styled, @tanstack/react-query, axios, @vizendjs/accent
pdfjs-dist, react-syntax-highlighter, react-zoom-pan-pinch, ag-grid-react, dayjs, lodash va jotai ro‘yxatdan yo‘qoldi. 150 qatorli komponentda 49 KB hajmdagi chunk paydo bo‘lishi g‘alati tuyuldi, ammo @vizendjs/vault-state ro‘yxatda yo‘qligini ko‘rganimda sababini tushundim. external faqat devDependencies va peerDependencies’ga nisbatan qo‘llanadi, vault-state esa dependency bo‘lgani uchun Stash’da fayllarni yuklab olish va ro‘yxatdan o‘tkazish uchun ishlatiladigan hook kodi chunk’ka kiritilgan. Qolgan beshta kutubxona chat ekranida allaqachon ishlatilgan, shuning uchun haqiqiy qo‘shimcha xarajat deyarli nolga teng edi. Raqamlardan ko‘ra ro‘yxatni tekshirish nimalar va nima sababdan yo‘qolganini aniq ko‘rsatdi. Shundan beri bundle’larni tekshirayotganda chunk hajmidan oldin ushbu ro‘yxatga qarayman.
8. Iste’molchi tomonida
Iste’molchi tomonida import path’iga /common qo‘shimchasini qo‘shishning o‘zi kifoya.
import { FileDownloadItem } from '@vizendjs/vault-view/common';
Biroq biz ushbu import’ni chat screen komponentida emas, yuqoriroq qatlam bo‘lgan collage’da joylashtirdik. Uni bevosita loopin-view’dan import qilish turli domenlardagi bir qatlamga mansub paketlar o‘rtasida dependency hosil qilib, monorepo qoidalarini buzardi. Dizaynni collage implementatsiyani qabul qilib, uni Context orqali inject qiladigan tarzda o‘zgartirish orqali vault-view versiyasi 7.2.6 dan 7.2.21 gacha yangilangan paytda ham loopin-view’ni qayta deploy qilish shart bo‘lmadi.
Yana bir ehtiyot bo‘lish kerak bo‘lgan jihat bor edi. Yengil entry ichkarida axios’ni bevosita import qiladi va bu axios host ilovasidagi bilan aynan bir xil instance bo‘lishi kerak. Biz autentifikatsiya tokenlari va actor identifikatorlarini axios’ning global instance’iga tegishli request interceptor’da biriktiramiz. Shu sababli paket o‘z nusxasidan foydalansa, bu so‘rovlar Authorization va Actorid’siz yuboriladi va 401 bilan yakunlanadi. Shuning uchun axios’ni dependencies emas, peerDependencies sifatida e’lon qildik.
9. Natijalar va o‘rganilgan saboqlar
|
Band |
Barrel entry |
Yengil entry |
|---|---|---|
|
Import path |
@vizendjs/vault-view |
@vizendjs/vault-view/common |
|
Referenced chunk size |
Taxminan 3.7 MB |
Taxminan 49 KB |
|
Iste’molchi ilova zimmasidagi tashqi kutubxonalar |
15 |
5 |
|
pdfjs-dist / pdf.worker |
Kiritilgan |
Mavjud emas |
Funksiya tomoni chatda alohida amalga oshirishsiz ham “chatda olingan fayllar mening fayl xotiramda qoladi” talabiga javob berdi. Biz 150 qatorli komponentni o‘z holicha qayta ishlatdik va shu bilan birga xarajatni kamaytirdik.
Paketning ommaviy APIʼsi eksportlar ro‘yxati emas, balki modullar grafigiga o‘xshaydi. `export *` bitta qatori paket muallifi uchun qulay, ammo isteʼmolchilar uchun uning ostidagi barcha narsalar xarajat sifatida qaytib keladi. Bundan buyon umumiy paketda barrel modul yaratganimda, avval “bu bilan birga nimalar yuklanadi?” degan savolni tekshiraman.
Umumiy paketda bir nechta kirish nuqtasi bo‘lishi mumkinligini bilib oldim. Avval mavjud kirish nuqtasini qandaydir tarzda yengillashtirishimiz kerak deb o‘ylagandim, ammo vault ilovasi barcha o‘sha og‘ir komponentlarga muhtoj bo‘lgan haqiqiy isteʼmolchi edi. Har bir isteʼmolchiga turli hajm kerak bo‘lganda, kirish nuqtalarini ajratish hamma uchun yaxshiroq tanlov bo‘ldi.
Hali tartibga solmagan narsalarim bor. Hozirda yengil kirish nuqtasiga nimalar kirishi mening mulohazamga bog‘liq, shuning uchun keyinchalik kimdir common.tsʼdan barrel modulni ehtiyotsizlik bilan import qilsa, u yana og‘irlashadi. Qurish vaqtida chunkʼning tashqi importlar ro‘yxatini tekshiradigan va u oldindan belgilangan sondan oshsa, jarayonni muvaffaqiyatsiz yakunlaydigan tekshiruv qo‘shmoqchiman, ammo uni qanday sozlash eng maʼqul ekanini ko‘rib chiqishim kerak.
messi