iT邦幫忙

2025 iThome 鐵人賽

DAY 25
0

Day 25: 前端開發的血淚教訓 - 從功能實現到視覺美學的漫長征途

前言:當功能完成後,真正的挑戰才開始

經過二十多天的開發,GASO (Google Apps Script Odyssey) 的核心功能已經基本完成。我們有了:

  • 完整的學習地圖生成系統
  • 智能搜尋功能
  • 路徑高亮顯示
  • 互動式節點操作

但是,當我開始調整前端畫面的美感時,才發現真正的挑戰才剛剛開始。今天,我想分享這段「前端地獄」的經歷,以及從中學到的寶貴教訓。

前端開發的三大難題

1. 難以言喻的「感覺」問題

做前端不像做後端一樣,只要把功能具體描述清楚就好了。做前端難就難在根本就很難講得清楚你要的那個感覺是什麼。

我雖然看得出來某一個設計的樣子很醜,但是你如果問我說:「欸,那比較美麗的樣子是什麼樣子?」我還真的沒辦法用具體的語言講出來。

具體案例:配色方案的選擇

在 Day 23 中,我們經歷了從現代化設計到古樸風格的轉型。這個過程讓我深刻體會到「感覺」的難以描述:

/* 現代化配色 - 看起來很「科技感」但缺乏溫度 */
background: rgba(52, 152, 219, 0.9);
color: #2c3e50;

/* 古樸配色 - 看起來很「溫暖」但需要精確調整 */
background: #8b4513;
color: #f4f1e8;

問題是:什麼是「科技感」?什麼是「溫暖」?這些都是主觀的感受,很難用程式碼來量化。

解決方案:建立視覺參考系統

我學會了建立視覺參考系統:

  1. 收集靈感圖片:從 Pinterest、Dribbble 等平台收集喜歡的設計
  2. 建立色彩調色盤:使用 Adobe Color 等工具建立一致的配色方案
  3. 建立設計系統:定義字體、間距、陰影等設計元素
/* 建立設計系統 */
:root {
  --primary-color: #8b4513;
  --secondary-color: #f4f1e8;
  --accent-color: #a0522d;
  --font-family: 'Times New Roman', 'Georgia', serif;
  --border-radius: 4px;
  --shadow: 0 4px 16px rgba(139, 69, 19, 0.3);
}

2. 多螢幕尺寸的惡夢

前端還有一個很難的地方就在於每一個人的螢幕大小、解析度又不一樣。

響應式設計的挑戰

在 GASO 的開發過程中,我們遇到了各種螢幕尺寸的問題:

/* 桌面版設計 */
.floating-zoom-controls {
  position: fixed;
  bottom: 20px;
  right: 20px;
  width: 40px;
  height: 40px;
}

/* 手機版適配 */
@media (max-width: 768px) {
  .floating-zoom-controls {
    bottom: 10px;
    right: 10px;
    width: 36px;
    height: 36px;
  }
}

但是,這只是開始。我們還需要考慮:

  • 平板電腦的尺寸
  • 超寬螢幕的顯示
  • 高解析度螢幕的像素密度
  • 不同瀏覽器的渲染差異

實際遇到的問題

  1. 縮放控制按鈕在手機上太小:使用者難以點擊
  2. 文字在小螢幕上過小:影響可讀性
  3. 地圖在超寬螢幕上顯示異常:比例失調

解決方案:漸進式增強

我學會了使用漸進式增強的方法:

/* 基礎樣式 - 適用於所有設備 */
.floating-zoom-controls {
  position: fixed;
  bottom: 20px;
  right: 20px;
  min-width: 40px;
  min-height: 40px;
}

/* 大螢幕優化 */
@media (min-width: 1200px) {
  .floating-zoom-controls {
    bottom: 30px;
    right: 30px;
    width: 50px;
    height: 50px;
  }
}

/* 小螢幕優化 */
@media (max-width: 768px) {
  .floating-zoom-controls {
    bottom: 10px;
    right: 10px;
    width: 36px;
    height: 36px;
  }
}

3. 多層容器的座標系統地獄

如果裡面有多個容器,一層包一層,每一個容器裡面的東西,它又有不同的參考座標,然後又會有不同的縮放比例。

座標系統的複雜性

在 Day 24 中,我們深入探討了座標系統的問題。這是一個極其複雜的多層系統:

// 多層座標系統
1. SVG 內部座標系統:Graphviz 生成的節點座標
2. CSS Transform 座標系統:zoomInner 的 translate 和 scale
3. 螢幕像素座標系統:瀏覽器視窗的實際像素
4. 容器座標系統:#graph 容器的尺寸和位置

具體的技術挑戰

// 嘗試置中節點的複雜計算
function centerNode(nodeId) {
  // 1. 取得節點在 SVG 中的位置
  const bbox = targetNodeElement.getBBox();
  const nodeX = bbox.x + bbox.width / 2;
  const nodeY = bbox.y + bbox.height / 2;
  
  // 2. 計算縮放後的位置
  const scale = state.scalePct / 100;
  const scaledNodeX = nodeX * scale;
  const scaledNodeY = nodeY * scale;
  
  // 3. 計算需要移動的距離
  const moveX = containerCenterX - scaledNodeX;
  const moveY = containerCenterY - scaledNodeY;
  
  // 4. 應用位移
  state.currentTranslateX = moveX;
  state.currentTranslateY = moveY;
}

這個看似簡單的功能,卻讓我陷入了座標系統的地獄,最終不得不採用更簡單的拖曳方案。

開發過程中的血淚教訓

1. 貪心的代價:一次改太多

最痛苦的是我常常貪心,一次想要改很多個地方,結果一改就改壞了,就有一些功能壞掉,但我也不知道到底是我改了哪一個部分影響到的。

具體案例:Day 23 的介面改造

在 Day 23 中,我同時進行了多項改動:

  • 改變配色方案
  • 重新設計布局
  • 移動縮放控制位置
  • 調整字體和間距

結果導致:

  • 縮放功能失效
  • 搜尋功能異常
  • 拖曳功能失靈

解決方案:小步快跑

我學會了「小步快跑」的開發方式:

  1. 一次只改一個功能:先改配色,測試無誤後再改布局
  2. 使用版本控制:每次改動都提交到 Git,方便回滾
  3. 建立測試清單:確保每次改動後所有功能都正常
# 使用 Git 進行小步提交
git add .
git commit -m "feat: 更新配色方案"
# 測試功能
git add .
git commit -m "feat: 調整布局結構"
# 測試功能

2. 前進後退的惡性循環

我就是這樣子前進後退,前進後退,前進五步,退後三步,然後前進三步又退後了五步。

具體案例:縮放功能的反覆修改

在縮放功能的開發中,我經歷了以下循環:

  1. 第一次嘗試:直接使用 transform: scale()

    • 問題:地圖變得過小
    • 結果:回滾到原始版本
  2. 第二次嘗試:設定容器尺寸 + transform: scale()

    • 問題:雙重縮放導致顯示異常
    • 結果:再次回滾
  3. 第三次嘗試:移除容器尺寸,只使用 transform: scale()

    • 問題:座標計算錯誤
    • 結果:又回滾
  4. 第四次嘗試:簡化為拖曳功能

    • 結果:終於成功

解決方案:建立開發日誌

我學會了建立詳細的開發日誌:

## 縮放功能開發日誌

### 2024-01-15 第一次嘗試
- 方法:直接使用 transform: scale()
- 問題:地圖變得過小
- 原因:沒有考慮容器尺寸
- 解決:回滾

### 2024-01-16 第二次嘗試
- 方法:設定容器尺寸 + transform: scale()
- 問題:雙重縮放
- 原因:同時設定了尺寸和縮放
- 解決:回滾

### 2024-01-17 第三次嘗試
- 方法:移除容器尺寸,只使用 transform: scale()
- 問題:座標計算錯誤
- 原因:座標系統複雜
- 解決:回滾

### 2024-01-18 第四次嘗試
- 方法:簡化為拖曳功能
- 結果:成功
- 原因:避開了複雜的座標計算

3. 功能與美學的平衡

這個專案在一開始做單純的功能的時候都很順,每天就做一些,每天就做一些,但是到了要調整前端畫面的美感的時候,根本就是三五天也調不了一點點。

功能開發 vs 美學調整

階段 時間投入 難度 成就感
功能開發 1-2天/功能 中等
美學調整 3-5天/調整

具體案例:搜尋功能的開發

功能開發階段(Day 9)

// 搜尋功能的核心邏輯
function searchNodes(query) {
  const results = state.nodeDetails.filter(node => 
    node.title.toLowerCase().includes(query.toLowerCase())
  );
  displaySearchResults(results);
}
  • 時間:1天
  • 難度:中等
  • 結果:功能完整

美學調整階段(Day 23-25)

/* 搜尋框的樣式調整 */
.search-container {
  background: linear-gradient(135deg, #f4f1e8 0%, #e8e0d0 100%);
  border: 2px solid #8b4513;
  box-shadow: 0 4px 16px rgba(139, 69, 19, 0.3);
  border-radius: 8px;
  padding: 12px;
}
  • 時間:3天
  • 難度:高
  • 結果:仍在調整中

從血淚中學到的智慧

1. 建立設計系統

問題:缺乏一致的設計標準

在開發過程中,我發現自己缺乏一致的設計標準,導致:

  • 顏色使用不一致
  • 間距設定混亂
  • 字體選擇隨意

解決方案:建立設計系統

/* 建立完整的設計系統 */
:root {
  /* 色彩系統 */
  --primary-color: #8b4513;
  --secondary-color: #f4f1e8;
  --accent-color: #a0522d;
  --text-color: #654321;
  --background-color: #faf8f3;
  
  /* 字體系統 */
  --font-family-primary: 'Times New Roman', 'Georgia', serif;
  --font-family-secondary: -apple-system, BlinkMacSystemFont, sans-serif;
  --font-size-small: 12px;
  --font-size-medium: 14px;
  --font-size-large: 16px;
  --font-size-xl: 20px;
  
  /* 間距系統 */
  --spacing-xs: 4px;
  --spacing-sm: 8px;
  --spacing-md: 16px;
  --spacing-lg: 24px;
  --spacing-xl: 32px;
  
  /* 陰影系統 */
  --shadow-sm: 0 2px 4px rgba(139, 69, 19, 0.1);
  --shadow-md: 0 4px 16px rgba(139, 69, 19, 0.3);
  --shadow-lg: 0 8px 32px rgba(139, 69, 19, 0.4);
}

2. 使用開發者工具進行診斷

問題:問題定位困難

當功能出現問題時,我常常不知道問題出在哪裡,只能盲目地修改程式碼。

解決方案:建立診斷系統

// 建立詳細的診斷系統
function debugContainerSizes() {
  const graph = document.getElementById('graph');
  const zoomInner = document.getElementById('zoomInner');
  
  console.log('=== 容器尺寸診斷 ===');
  console.log('Graph 容器:', {
    clientWidth: graph.clientWidth,
    clientHeight: graph.clientHeight,
    offsetWidth: graph.offsetWidth,
    offsetHeight: graph.offsetHeight
  });
  
  console.log('ZoomInner 容器:', {
    clientWidth: zoomInner.clientWidth,
    clientHeight: zoomInner.clientHeight,
    transform: zoomInner.style.transform
  });
}

// 在關鍵時刻調用診斷
function centerNode(nodeId) {
  console.log('=== 開始置中節點 ===');
  debugContainerSizes();
  
  // ... 置中邏輯
  
  setTimeout(() => {
    console.log('=== 置中後驗證 ===');
    debugContainerSizes();
  }, 100);
}

3. 建立測試清單

問題:改動後不知道哪些功能受影響

每次修改後,我都不確定哪些功能可能受到影響。

解決方案:建立功能測試清單

## 功能測試清單

### 基本功能
- [ ] 地圖載入正常
- [ ] 節點顯示正確
- [ ] 連線顯示正確

### 互動功能
- [ ] 節點點擊正常
- [ ] 搜尋功能正常
- [ ] 路徑高亮正常

### 縮放功能
- [ ] 放大按鈕正常
- [ ] 縮小按鈕正常
- [ ] 重設縮放正常
- [ ] 觀看全地圖正常

### 拖曳功能
- [ ] 地圖拖曳正常
- [ ] 拖曳邊界限制正常
- [ ] 拖曳後縮放正常

### 響應式設計
- [ ] 桌面版顯示正常
- [ ] 平板版顯示正常
- [ ] 手機版顯示正常

4. 學會妥協和簡化

問題:追求完美導致複雜化

我常常追求完美的解決方案,結果讓問題變得更加複雜。

解決方案:學會妥協

在 Day 24 中,我學會了妥協:

// 原本想要的自動置中功能(複雜)
function centerNode(nodeId) {
  // 複雜的座標計算
  const bbox = node.getBBox();
  const nodeX = bbox.x + bbox.width / 2;
  const nodeY = bbox.y + bbox.height / 2;
  // ... 更多複雜計算
}

// 最終採用的拖曳方案(簡單)
document.addEventListener('mousedown', function(e) {
  if (e.target.closest('svg') && !e.target.closest('g.node')) {
    startDrag(e);
  }
});

有時候,簡單的解決方案比複雜的完美方案更好。

實用的開發建議

1. 前端開發的最佳實踐

使用 CSS 變數

/* 使用 CSS 變數提高可維護性 */
:root {
  --primary-color: #8b4513;
  --secondary-color: #f4f1e8;
}

.button {
  background-color: var(--primary-color);
  color: var(--secondary-color);
}

建立響應式設計

/* 移動優先的響應式設計 */
.container {
  padding: 16px;
}

@media (min-width: 768px) {
  .container {
    padding: 24px;
  }
}

@media (min-width: 1200px) {
  .container {
    padding: 32px;
  }
}

使用語義化的 HTML

<!-- 使用語義化的 HTML 結構 -->
<header class="header">
  <h1>GASO - Google Apps Script Odyssey</h1>
  <nav class="navigation">
    <button class="search-btn">搜尋</button>
  </nav>
</header>

<main class="main-content">
  <div class="graph-container" id="graph">
    <!-- 地圖內容 -->
  </div>
</main>

<aside class="sidebar">
  <!-- 側邊面板 -->
</aside>

2. JavaScript 的最佳實踐

使用模組化開發

// 將功能分離到不同的模組
const SearchModule = {
  init() {
    this.bindEvents();
  },
  
  bindEvents() {
    document.getElementById('search-btn').addEventListener('click', this.handleSearch.bind(this));
  },
  
  handleSearch(event) {
    // 搜尋邏輯
  }
};

const ZoomModule = {
  init() {
    this.bindEvents();
  },
  
  bindEvents() {
    document.getElementById('zoom-in').addEventListener('click', this.zoomIn.bind(this));
  },
  
  zoomIn() {
    // 縮放邏輯
  }
};

使用事件委託

// 使用事件委託提高效能
document.addEventListener('click', function(e) {
  if (e.target.matches('.zoom-btn')) {
    handleZoom(e.target);
  } else if (e.target.matches('.node')) {
    handleNodeClick(e.target);
  }
});

建立狀態管理

// 建立統一的狀態管理
const AppState = {
  scalePct: 100,
  currentTranslateX: 0,
  currentTranslateY: 0,
  nodeDetails: [],
  
  updateScale(newScale) {
    this.scalePct = newScale;
    this.notify('scaleChanged', newScale);
  },
  
  updateTranslate(x, y) {
    this.currentTranslateX = x;
    this.currentTranslateY = y;
    this.notify('translateChanged', { x, y });
  },
  
  notify(event, data) {
    // 通知其他模組狀態變化
  }
};

3. 除錯和測試的最佳實踐

建立除錯模式

// 建立除錯模式
const DEBUG = true;

function debugLog(message, data) {
  if (DEBUG) {
    console.log(`[DEBUG] ${message}`, data);
  }
}

function centerNode(nodeId) {
  debugLog('開始置中節點', { nodeId });
  
  // ... 置中邏輯
  
  debugLog('置中完成', { 
    finalX: state.currentTranslateX, 
    finalY: state.currentTranslateY 
  });
}

建立視覺化除錯工具

// 建立視覺化除錯工具
function showDebugInfo() {
  const debugPanel = document.createElement('div');
  debugPanel.id = 'debug-panel';
  debugPanel.style.cssText = `
    position: fixed;
    top: 10px;
    left: 10px;
    background: rgba(0,0,0,0.8);
    color: white;
    padding: 10px;
    font-family: monospace;
    font-size: 12px;
    z-index: 9999;
  `;
  
  document.body.appendChild(debugPanel);
  
  setInterval(() => {
    const graph = document.getElementById('graph');
    const zoomInner = document.getElementById('zoomInner');
    
    debugPanel.innerHTML = `
      縮放比例: ${state.scalePct}%<br>
      位移: (${state.currentTranslateX}, ${state.currentTranslateY})<br>
      容器尺寸: ${graph.clientWidth}x${graph.clientHeight}<br>
      SVG 尺寸: ${zoomInner.clientWidth}x${zoomInner.clientHeight}
    `;
  }, 100);
}

避免踩坑的經驗總結

1. 設計階段的準備工作

建立設計系統

在開始開發之前,先建立完整的設計系統:

/* 在開始開發前就定義好設計系統 */
:root {
  /* 色彩系統 */
  --primary-color: #8b4513;
  --secondary-color: #f4f1e8;
  --accent-color: #a0522d;
  
  /* 字體系統 */
  --font-family: 'Times New Roman', 'Georgia', serif;
  --font-size-base: 14px;
  
  /* 間距系統 */
  --spacing-unit: 8px;
  --spacing-xs: calc(var(--spacing-unit) * 0.5);
  --spacing-sm: var(--spacing-unit);
  --spacing-md: calc(var(--spacing-unit) * 2);
  --spacing-lg: calc(var(--spacing-unit) * 3);
  --spacing-xl: calc(var(--spacing-unit) * 4);
}

建立響應式斷點

/* 定義響應式斷點 */
:root {
  --breakpoint-sm: 576px;
  --breakpoint-md: 768px;
  --breakpoint-lg: 992px;
  --breakpoint-xl: 1200px;
}

@media (min-width: var(--breakpoint-md)) {
  /* 平板樣式 */
}

@media (min-width: var(--breakpoint-lg)) {
  /* 桌面樣式 */
}

2. 開發階段的注意事項

小步快跑

# 每次只做一個小改動
git add .
git commit -m "feat: 更新按鈕顏色"
# 測試功能
git add .
git commit -m "feat: 調整按鈕間距"
# 測試功能

建立測試清單

## 每次改動後的測試清單
- [ ] 基本功能正常
- [ ] 響應式設計正常
- [ ] 不同瀏覽器正常
- [ ] 效能沒有明顯下降

使用版本控制

# 建立功能分支
git checkout -b feature/new-design
# 進行改動
git add .
git commit -m "feat: 新設計"
# 測試無誤後合併
git checkout main
git merge feature/new-design

3. 除錯階段的技巧

建立除錯工具

// 建立除錯工具
const DebugTools = {
  showContainerInfo() {
    const containers = ['graph', 'zoomInner', 'header'];
    containers.forEach(id => {
      const el = document.getElementById(id);
      if (el) {
        console.log(`${id}:`, {
          clientWidth: el.clientWidth,
          clientHeight: el.clientHeight,
          offsetWidth: el.offsetWidth,
          offsetHeight: el.offsetHeight,
          transform: el.style.transform
        });
      }
    });
  },
  
  showStateInfo() {
    console.log('App State:', {
      scalePct: state.scalePct,
      currentTranslateX: state.currentTranslateX,
      currentTranslateY: state.currentTranslateY,
      nodeDetails: state.nodeDetails.length
    });
  }
};

使用瀏覽器開發者工具

  1. Elements 面板:檢查 DOM 結構和 CSS 樣式
  2. Console 面板:查看 JavaScript 錯誤和日誌
  3. Network 面板:監控網路請求和資源載入
  4. Performance 面板:分析效能問題
  5. Responsive 面板:測試響應式設計

4. 優化階段的策略

效能優化

// 使用防抖函數優化搜尋
function debounce(func, wait) {
  let timeout;
  return function executedFunction(...args) {
    const later = () => {
      clearTimeout(timeout);
      func(...args);
    };
    clearTimeout(timeout);
    timeout = setTimeout(later, wait);
  };
}

const debouncedSearch = debounce(searchNodes, 300);

記憶體優化

// 清理事件監聽器
function cleanup() {
  document.removeEventListener('click', handleClick);
  document.removeEventListener('resize', handleResize);
}

// 在頁面卸載時清理
window.addEventListener('beforeunload', cleanup);

結語:前端開發的智慧

經過這二十多天的開發,我深刻體會到前端開發的複雜性和挑戰性。從功能實現到視覺美學,每一步都需要仔細的規劃和執行。

最重要的教訓

  1. 建立設計系統:在開始開發前就定義好設計標準
  2. 小步快跑:每次只做一個小改動,避免大規模的修改
  3. 學會妥協:有時候簡單的解決方案比複雜的完美方案更好
  4. 建立測試清單:確保每次改動後所有功能都正常
  5. 使用除錯工具:建立完善的除錯和診斷系統

給未來開發者的建議

  1. 不要貪心:一次只改一個功能,測試無誤後再繼續
  2. 建立參考:收集靈感圖片,建立視覺參考系統
  3. 使用工具:善用瀏覽器開發者工具和除錯工具
  4. 保持耐心:前端開發需要時間和耐心,不要急於求成
  5. 持續學習:前端技術變化很快,要保持學習的習慣

最後的思考

前端開發不僅是技術的實現,更是藝術與科學的結合。它需要我們:

  • 理解使用者的需求
  • 掌握技術的細節
  • 平衡功能與美學
  • 處理複雜的相容性問題

在這個過程中,我們會遇到挫折,會感到困惑,但每一次的挑戰都是成長的機會。當我們最終看到一個美觀、實用的介面時,所有的努力都是值得的。

前端開發的智慧,不在於追求完美,而在於在限制中找到最佳的平衡點。


在 GASO 的開發旅程中,每一天都是新的學習,每一次挑戰都是成長的機會。讓我們帶著今天的智慧,繼續前進!🚀


上一篇
Day 24: 我失敗了。試了半天之後,我還是失敗了。
系列文
亨利羊帶你 Google Apps Script 從入門到精通:放棄長篇大論的教學吧,你需要的只是一些精心設計的 prompt!25
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言