經過二十多天的開發,GASO (Google Apps Script Odyssey) 的核心功能已經基本完成。我們有了:
但是,當我開始調整前端畫面的美感時,才發現真正的挑戰才剛剛開始。今天,我想分享這段「前端地獄」的經歷,以及從中學到的寶貴教訓。
做前端不像做後端一樣,只要把功能具體描述清楚就好了。做前端難就難在根本就很難講得清楚你要的那個感覺是什麼。
我雖然看得出來某一個設計的樣子很醜,但是你如果問我說:「欸,那比較美麗的樣子是什麼樣子?」我還真的沒辦法用具體的語言講出來。
在 Day 23 中,我們經歷了從現代化設計到古樸風格的轉型。這個過程讓我深刻體會到「感覺」的難以描述:
/* 現代化配色 - 看起來很「科技感」但缺乏溫度 */
background: rgba(52, 152, 219, 0.9);
color: #2c3e50;
/* 古樸配色 - 看起來很「溫暖」但需要精確調整 */
background: #8b4513;
color: #f4f1e8;
問題是:什麼是「科技感」?什麼是「溫暖」?這些都是主觀的感受,很難用程式碼來量化。
我學會了建立視覺參考系統:
/* 建立設計系統 */
: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);
}
前端還有一個很難的地方就在於每一個人的螢幕大小、解析度又不一樣。
在 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;
}
}
但是,這只是開始。我們還需要考慮:
我學會了使用漸進式增強的方法:
/* 基礎樣式 - 適用於所有設備 */
.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;
}
}
如果裡面有多個容器,一層包一層,每一個容器裡面的東西,它又有不同的參考座標,然後又會有不同的縮放比例。
在 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;
}
這個看似簡單的功能,卻讓我陷入了座標系統的地獄,最終不得不採用更簡單的拖曳方案。
最痛苦的是我常常貪心,一次想要改很多個地方,結果一改就改壞了,就有一些功能壞掉,但我也不知道到底是我改了哪一個部分影響到的。
在 Day 23 中,我同時進行了多項改動:
結果導致:
我學會了「小步快跑」的開發方式:
# 使用 Git 進行小步提交
git add .
git commit -m "feat: 更新配色方案"
# 測試功能
git add .
git commit -m "feat: 調整布局結構"
# 測試功能
我就是這樣子前進後退,前進後退,前進五步,退後三步,然後前進三步又退後了五步。
在縮放功能的開發中,我經歷了以下循環:
第一次嘗試:直接使用 transform: scale()
第二次嘗試:設定容器尺寸 + transform: scale()
第三次嘗試:移除容器尺寸,只使用 transform: scale()
第四次嘗試:簡化為拖曳功能
我學會了建立詳細的開發日誌:
## 縮放功能開發日誌
### 2024-01-15 第一次嘗試
- 方法:直接使用 transform: scale()
- 問題:地圖變得過小
- 原因:沒有考慮容器尺寸
- 解決:回滾
### 2024-01-16 第二次嘗試
- 方法:設定容器尺寸 + transform: scale()
- 問題:雙重縮放
- 原因:同時設定了尺寸和縮放
- 解決:回滾
### 2024-01-17 第三次嘗試
- 方法:移除容器尺寸,只使用 transform: scale()
- 問題:座標計算錯誤
- 原因:座標系統複雜
- 解決:回滾
### 2024-01-18 第四次嘗試
- 方法:簡化為拖曳功能
- 結果:成功
- 原因:避開了複雜的座標計算
這個專案在一開始做單純的功能的時候都很順,每天就做一些,每天就做一些,但是到了要調整前端畫面的美感的時候,根本就是三五天也調不了一點點。
階段 | 時間投入 | 難度 | 成就感 |
---|---|---|---|
功能開發 | 1-2天/功能 | 中等 | 高 |
美學調整 | 3-5天/調整 | 高 | 低 |
功能開發階段(Day 9):
// 搜尋功能的核心邏輯
function searchNodes(query) {
const results = state.nodeDetails.filter(node =>
node.title.toLowerCase().includes(query.toLowerCase())
);
displaySearchResults(results);
}
美學調整階段(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;
}
在開發過程中,我發現自己缺乏一致的設計標準,導致:
/* 建立完整的設計系統 */
: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);
}
當功能出現問題時,我常常不知道問題出在哪裡,只能盲目地修改程式碼。
// 建立詳細的診斷系統
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);
}
每次修改後,我都不確定哪些功能可能受到影響。
## 功能測試清單
### 基本功能
- [ ] 地圖載入正常
- [ ] 節點顯示正確
- [ ] 連線顯示正確
### 互動功能
- [ ] 節點點擊正常
- [ ] 搜尋功能正常
- [ ] 路徑高亮正常
### 縮放功能
- [ ] 放大按鈕正常
- [ ] 縮小按鈕正常
- [ ] 重設縮放正常
- [ ] 觀看全地圖正常
### 拖曳功能
- [ ] 地圖拖曳正常
- [ ] 拖曳邊界限制正常
- [ ] 拖曳後縮放正常
### 響應式設計
- [ ] 桌面版顯示正常
- [ ] 平板版顯示正常
- [ ] 手機版顯示正常
我常常追求完美的解決方案,結果讓問題變得更加複雜。
在 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);
}
});
有時候,簡單的解決方案比複雜的完美方案更好。
/* 使用 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 結構 -->
<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>
// 將功能分離到不同的模組
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) {
// 通知其他模組狀態變化
}
};
// 建立除錯模式
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);
}
在開始開發之前,先建立完整的設計系統:
/* 在開始開發前就定義好設計系統 */
: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)) {
/* 桌面樣式 */
}
# 每次只做一個小改動
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
// 建立除錯工具
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
});
}
};
// 使用防抖函數優化搜尋
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);
經過這二十多天的開發,我深刻體會到前端開發的複雜性和挑戰性。從功能實現到視覺美學,每一步都需要仔細的規劃和執行。
前端開發不僅是技術的實現,更是藝術與科學的結合。它需要我們:
在這個過程中,我們會遇到挫折,會感到困惑,但每一次的挑戰都是成長的機會。當我們最終看到一個美觀、實用的介面時,所有的努力都是值得的。
前端開發的智慧,不在於追求完美,而在於在限制中找到最佳的平衡點。
在 GASO 的開發旅程中,每一天都是新的學習,每一次挑戰都是成長的機會。讓我們帶著今天的智慧,繼續前進!🚀