iT邦幫忙

2026 iThome 鐵人賽

DAY 3
0
Software Development

從零打造網頁簡報編輯器系列 第 3

Day 03|一張投影片需要哪些資料

  • 分享至 

  • xImage
  •  

系列:一張投影片背後的工程:從零打造網頁簡報編輯器

對應程式版本:Git tag day-03

版本說明:本文記錄當日實作;後續版本增加的物件與欄位不改變這裡的資料分層原則。

畫面上的一張白色投影片,看起來只需要幾個文字框和圖片。但只要加入切換、排序、保存與列印,資料結構就必須回答更多問題:哪一張排在前面?物件屬於哪一頁?圖片內容存在哪裡?縮放比例要不要跟著保存?

上一篇把第一版拆成可以驗收的里程碑。這次先處理所有功能都會依賴的文件模型,並把「需要重新開啟的內容」與「只在當下操作有用的狀態」分開。

先問哪些資料需要留下來

判斷方式很直接:關閉瀏覽器再開啟後,這項資料是否仍然有意義?投影片順序、文字內容、圖片尺寸與位置需要保留;目前選取框、文字焦點和畫布縮放則不需要。

文件資料 介面狀態
簡報名稱與格式版本 目前選取的物件
畫布邏輯尺寸 正在編輯的文字框
投影片順序與背景 拖曳中的暫時位置
圖文內容、位置及尺寸 符合視窗或百分比縮放
圖片資產 ID 儲存中與錯誤提示

文件資料必須可序列化,並能交給編輯器、縮圖、列印與 IndexedDB 共用。介面狀態只由 React 管理,避免無意義的操作資訊進入儲存格式。

文件、投影片與物件

我先把資料分成簡報文件、投影片和投影片物件三層。簡化後的 TypeScript 型別如下:

interface PresentationDocument {
  schemaVersion: 1
  name: string
  canvas: { width: number; height: number }
  slideOrder: string[]
  slides: Record<string, Slide>
}

interface Slide {
  id: string
  background: string
  elements: SlideElement[]
}

schemaVersion 讓程式讀取資料時能辨識格式;目前只有版本 1,尚未實作格式升級。畫布固定使用 1600 × 900 邏輯座標,所以物件的 xywidthheight 不會因為使用者把畫面縮放成 50% 而改變。顯示與指標位移才套用縮放比例。

順序與內容分開保存

slideOrder 只保存投影片 ID,slides 則用 ID 找到完整內容。例如順序從 ['slide-a', 'slide-b'] 改成 ['slide-b', 'slide-a'],兩張投影片的內容都不需要搬動。

排序時只移動陣列中的 ID;刪除時再同步移除順序與對應紀錄。固定 ID 讓目前投影片、列印頁面與之後的操作歷史可以指向同一份資料,不受頁碼改變影響。這也比把投影片深層巢狀在多個介面狀態中更容易做不可變更新。

這一輪新增的 slide/addslide/moveslide/delete 都是純 reducer action。加入投影片可以指定插入位置,移動只更新 slideOrder,刪除會拒絕移除文件中的最後一張投影片。介面上的目前頁面仍由 React state 保存,切頁後不會污染可序列化文件。

用聯集描述文字與圖片

文字和圖片都有 idxywidthheight,但各自保留需要的欄位:

// 以下省略兩者共用的 id、x、y、width、height
interface TextElement {
  type: 'text'
  text: string
  style: { fontSize: number; color: string; textAlign: TextAlign }
}

interface ImageElement {
  type: 'image'
  assetId: string
  alt: string
}

type SlideElement = TextElement | ImageElement

type 是可辨識聯集的判別欄位。程式確認 element.type === 'text' 後,TypeScript 就知道這個物件一定有 textstyle;圖片分支則能使用 assetId。後續加入圖形物件時,也沿用 type 分支擴充渲染與更新邏輯。

圖片內容與文件分開

圖片 Blob 沒有直接塞入簡報文件。它獨立存進 IndexedDB,文件只保留資產 ID。複製投影片時,每張投影片和每個物件都取得新 ID,圖片物件仍引用同一份資產。這避免複製大圖,也為後續操作歷史控制記憶體用量。

https://ithelp.ithome.com.tw/upload/images/20260913/20182759oqfyKpg991.png

畫面取自後續版本:左側列出 presentationsassets 兩個儲存區,右側顯示簡報文件紀錄;圖片 Blob 沒有在這張截圖中展開。這項資料分層與 day-03 相同。

這項選擇也有代價:匯入圖片時,先寫入 Blob,成功後才把 assetId 加進文件;當時兩次寫入並非同一筆交易。刪除圖片物件時,也不能直接刪除 Blob,因為其他投影片可能還在引用。後續的專案備份只收集文件目前引用的圖片;自動清理未引用資產仍未實作。

TypeScript 型別還不等於匯入驗證

TypeScript 能檢查專案裡的程式碼,卻不能保證 IndexedDB 或未來匯入的 JSON 一定正確。因此讀取資料時仍要從 unknown 開始,由 isPresentationDocument 檢查格式版本、畫布、投影片順序及各種物件欄位,通過後才能替換目前文件。

day-03 的驗證器先處理第一版所需的結構與型別,當時還沒有檢查重複 ID、圖片引用是否存在、有限數字和尺寸範圍。後續版本在文件與備份匯入時,補上投影片及單頁物件的重複 ID、備份圖片引用、有限數字與尺寸邊界檢查。把各階段的限制寫清楚,可以避免把編譯期型別誤當成完整的資料安全保證。

day-03 標籤中共有 6 個測試檔、24 個案例通過。其中 reducer 測試會驗證投影片插入、移動、刪除對應紀錄及禁止刪除最後一頁;介面測試確認複製後仍保留內容,IndexedDB 測試則確認文件與圖片 Blob 可以完整往返。

下一篇會使用這份資料建立最小投影片畫面,說明一筆文字物件如何變成可見的 DOM 元素。

參考資料


上一篇
Day 02|把簡報工具拆成可以完成的工作
下一篇
Day 04|建立第一張可以呈現內容的投影片
系列文
從零打造網頁簡報編輯器13
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言