#專案起源:一個溫暖的想法
在資訊管理系的學習過程中,我發現身邊許多同學都有記錄心情、整理思緒的需求。市面上的日記應用雖多,卻常常不是功能繁雜難用,就是隱私保護不足。
於是,日記之森 (SoulCraft Journal) 的想法在我心中萌芽了。
設計初衷
- 溫暖療癒:以森林主題營造寧靜的書寫氛圍
- 隱私安全:用戶的情感記錄理應受到完整保護
- 專注書寫:簡潔的介面讓用戶安心記錄
- AI 陪伴:以智能聊天機器人提供溫暖的互動
#技術選型的學習歷程
當時的我:架構?那是什麼?
回想專案初期,老實說我連「架構」是什麼都不太懂。聽著學長姐談論 MVC、前後端分離,我完全一頭霧水。
但也正是這份「無知者無畏」的心態,讓我下定決心從最基礎的技術學起。
為什麼選擇 Flask?
- 學習曲線友善:對初學者相對容易上手。
- 文件豐富:網路上有大量中文教學資源。
- 靈活度高:能依自己的需求逐步擴展功能。
- Python 生態:與我在課堂上學的 Python 無縫接軌。
最終的技術組合
核心架構
├── Flask (Python) - Web 框架
├── Jinja2 - 模板引擎
├── Flask-SocketIO - 即時通訊
├── PostgreSQL - 資料庫
└── HTML/CSS/JavaScript - 前端技術
功能模組
├── 使用者模組 - 註冊登入、個人設定
├── 日記模組 - 撰寫、查看、管理
├── AI 互動模組 - 智能聊天機器人
└── 管理員後台 - 內容管理、客服系統#從零開始的學習之路
第一階段:搞懂「大架構」是什麼
還記得剛開始時,我對 Web 應用程式的基本概念十分模糊:什麼是前端?什麼是後端?為什麼需要資料庫?
於是我花了好幾週,從最基礎的概念開始一點一滴研究:
前端 (Frontend):
- 用戶看得到、摸得著的介面
- HTML 負責結構,CSS 負責樣式,JavaScript 負責互動
後端 (Backend):
- 處理業務邏輯,管理資料
- Flask 框架幫我處理路由和請求
資料庫 (Database):
- 儲存用戶資料和日記內容
- PostgreSQL 提供穩定的關聯式資料庫支援
第二階段:模組化設計的領悟
慢慢理解基本概念後,下一個挑戰是如何組織程式碼。起初我把所有功能都塞進同一個檔案,結果越寫越亂。
後來學會了 Flask Blueprints 的概念,開始將功能模組化:
services/ # 按功能分模組
├── user/ # 使用者相關功能
├── diary/ # 日記功能
├── ai/ # AI 聊天功能
└── admin/ # 管理員後台這樣的設計讓我領會到一個重要概念:關注點分離。每個模組只專注處理自己的業務邏輯,程式碼也因此更容易理解與維護。
第三階段:從小功能到完整系統
有了大架構的概念後,我開始一個功能一個功能地實作:
#實作過程的心境轉折
使用者認證:我的第一個「大功能」
還記得第一次要實作用戶註冊登入功能時,我完全不知道從何下手。什麼是 Session?什麼又是密碼加密?
我花了整整一週,從網路教學中一點一滴拼湊出一套基本的認證系統:
學到的重要概念:
- 密碼不能明文儲存:學會使用雜湊函數保護用戶密碼
- Session 管理:理解如何追蹤用戶的登入狀態
- 表單驗證:前端驗證提升體驗,後端驗證確保安全
當「註冊成功」的訊息第一次出現在畫面上時,那份成就感真的難以言喻。
日記撰寫功能:從簡單到豐富
最初的日記功能非常陽春,只是一個文字框加上儲存按鈕。但隨著對用戶需求理解加深,我開始思考:
用戶真正需要什麼?
- 情緒記錄:不只是文字,還要能記錄當時的心情
- 隱私保護:確保只有用戶自己能看到自己的日記
- 搜尋功能:日記多了之後,要能快速找到想要的內容
每新增一個功能,我都得學習一項新技術。情緒標記讓我學會資料庫的枚舉型別,搜尋功能則讓我理解了 SQL 的模糊查詢。
AI 聊天機器人:最具挑戰性的功能
專案進行到中期,我想加入一個 AI 聊天機器人,讓用戶在寫日記之餘,還能有個「數位夥伴」相伴。
技術挑戰:
- API 整合:學習如何串接外部 AI 服務
- 即時通訊:使用 Flask-SocketIO 實現即時對話
- 對話管理:如何儲存和管理對話歷史
心境轉折:
這個功能讓我第一次體會到「系統整合」的複雜:它不再是單純的 CRUD 操作,而是要讓不同的服務協同運作。
雖然最終的實作相對簡單,但這段過程讓我對「全端開發」有了更深的理解。
#踩坑經驗分享
1. 狀態管理的坑
問題:一開始用 useState 管理所有狀態,導致組件重渲染過多
// 錯誤做法
const [entries, setEntries] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const [currentEntry, setCurrentEntry] = useState(null);
// ... 更多狀態
// 正確做法:使用 useReducer
const [state, dispatch] = useReducer(journalReducer, initialState);
const journalReducer = (state, action) => {
switch (action.type) {
case 'SET_LOADING':
return { ...state, loading: action.payload };
case 'SET_ENTRIES':
return { ...state, entries: action.payload, loading: false };
case 'ADD_ENTRY':
return {
...state,
entries: [action.payload, ...state.entries]
};
default:
return state;
}
};2. API 錯誤處理
問題:沒有統一的錯誤處理機制
// 統一的 API 錯誤處理
const apiCall = async (url, options = {}) => {
try {
const response = await fetch(url, {
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${getToken()}`,
...options.headers
},
...options
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.message || '請求失敗');
}
return await response.json();
} catch (error) {
console.error('API 呼叫錯誤:', error);
throw error;
}
};3. 效能優化
// 使用 React.memo 避免不必要的重渲染
const EntryCard = React.memo(({ entry, onEdit, onDelete }) => {
return (
<div className="entry-card">
<h3>{entry.title}</h3>
<p>{entry.excerpt}</p>
<div className="actions">
<button onClick={() => onEdit(entry.id)}>編輯</button>
<button onClick={() => onDelete(entry.id)}>刪除</button>
</div>
</div>
);
});
// 虛擬化長列表
import { FixedSizeList as List } from 'react-window';
const EntryList = ({ entries }) => (
<List
height={600}
itemCount={entries.length}
itemSize={120}
itemData={entries}
>
{EntryCard}
</List>
);#用戶回饋與迭代
用戶痛點與改進
- 載入速度:實作骨架屏和圖片懶加載
- 離線功能:添加 Service Worker 支援
- 備份功能:提供資料匯出選項
#下一步規劃
技術升級
- 遷移到 Next.js 15
- 導入 React Server Components
- 實作 PWA 功能
- 添加 AI 寫作建議
功能擴展
- 多媒體日記支援
- 社群分享功能(匿名)
- 情緒趨勢分析
- 提醒與習慣養成
#給同路人的建議
- 先做 MVP:不要想著一次做完所有功能
- 用戶回饋很重要:early adopters 的意見是金
- 記錄開發過程:寫下踩坑經驗,幫助未來的自己
- 保持學習心態:技術選型沒有標準答案,適合的就是最好的
#結語
從一個想法走到實際的產品,這段過程充滿挑戰,卻也格外有成就感。日記之森不只是我的第一個全端專案,更是我成長路上的重要里程碑。
每一行程式碼都承載著我想幫助他人的初心,每一項功能都是對更好用戶體驗的追求。
體驗專案:SoulCraft Journal
技術交流:john_lu@intellitrustme.com
更多文章:持續分享開發心得與技術筆記