🚀 來喔!想做自己的 SaaS?這個「SaaS 樣板」讓你少走很多冤枉路啦!
各位辛苦的開發者同胞們,大家好!
說到做軟體服務(SaaS),很多人心裡都會癢癢的,想著能把自己的好點子變成一個實用的線上工具。但要從頭開始,從前端的 Vue 弄到後端的 Laravel,還要搞懂 Docker、CI/CD 那些眉角,實在有點費心神,常常是程式還沒開始寫,光搞環境就飽了。
所以啦,這次特別整理了一個我親手搭的「SaaS Boilerplate」。它就像是幫你把地基跟骨架都蓋好了,讓你直接可以往上蓋房子,專心把心思放在你最厲害的業務邏輯上。這個專案整合了當前流行的技術棧:Laravel 後端 API、Vue 3 前端 SPA,並搭配 Docker 容器化 與 GitHub Actions CI/CD,讓你能夠專注於核心業務邏輯,而非繁瑣的基礎設施搭建。
這個樣板有啥好處?聽我細細說來:
- 前後端分開跑,互不干擾:後端用 Laravel 來處理資料跟 API,穩穩的;前端用 Vue 3 負責介面,用起來順手又好看。各做各的,以後想換技術也比較方便。
- 用戶跟訂閱管理,基礎都打好了:裡頭已經有用戶、方案(Plan)和訂閱(Subscription)的基本架構,你想接金流服務(像 Stripe 那種),或是搞什麼試用期、續約、升降級,都有個底子在,不用從零開始。
- 管理員專區,給你傳便便:也做了個簡單的管理介面,讓你這個頭家可以管管用戶、看看方案,輕鬆掌握狀況。
- 權限分明,讓你免操煩:後端用 Laravel 的「Gate」在把關,只有有權限的人才能動重要的東西;前端 Vue 也有「Router 守衛」,雙重保險,讓你放心。
- 環境交給 Docker,省時省力:所有哩哩叩叩的服務(PHP、Nginx、資料庫、快取、甚至連測試信箱 MailHog),通通包進 Docker 裡,一個指令就搞定。再也不用擔心「怎麼我電腦可以跑,你那邊就不行?」這種氣話了。
- 程式碼品質,GitHub Actions 幫你顧:每次程式碼丟上去,GitHub Actions 就會自動幫你檢查程式碼有沒有問題、測試合不合格。這樣大家寫出來的程式,品質才會都「足感心」。
- API 文件,懶人包也給你:接了 L5-Swagger,API 文件會自動生出來,要對接或是測試,一看就懂,不用再手寫半天。
這個專案,就是希望讓你把力氣花在刀口上,專注把產品做到最好。
📦 話不多說,來實作一下,給你看怎麼快速啟動!
開始前,記得你的電腦要有裝 Docker 跟 Docker Compose 喔!
第一步:把專案領回家
打開你的終端機,把這個寶貝專案領回來:
git clone https://github.com/BpsEason/saas-boilerplate.git
cd saas-boilerplate
(溫馨提醒:如果你想給這個新專案一個專屬的名字,可以直接用我們準備的 ./create_project.sh
腳本,它會自動完成很多設定喔!)
# 比如說,你的專案叫 '我的神隊友SaaS'
./create_project.sh MyAwesomeSaaS
cd MyAwesomeSaaS # 記得進到新的專案資料夾喔!
第二步:啟動 Docker 大哥開工
專案資料夾裡,下這個指令,讓 Docker 把所有服務都「歐噴」起來。--build
是確保用最新的設定來啟動。
docker compose up -d --build
這時候,後端 PHP、Nginx 網頁伺服器、MySQL 資料庫、Redis 快取,還有讓你測試寄信的 MailHog,全部都會乖乖跑起來。
第三步:設定後端與 API 文件
現在我們需要進入 Laravel 容器,執行一些初始化命令。特別提醒:L5-Swagger 的 API 文件不會自動生成,你需要手動執行命令讓它「生」出來喔!
# 進入後端 Laravel 的「家」
docker compose exec app bash
# 生成應用程式的「金鑰」,很重要,不要漏掉!
php artisan key:generate
# 跑資料庫的結構更新,順便把一些初始資料(包含管理員帳號)也放進去
php artisan migrate --seed
# 生成 API 文件! (這需要你在 Controller 中有撰寫 Swagger 註解)
php artisan l5-swagger:generate
# 事情辦好了,出來透透氣
exit
預設管理員帳號底家啦:
- 信箱 (Email):
admin@example.com
- 密碼 (Password):
password
第四步:啟動前端 Vue 小弟也來動一動
前端這邊就交給 Vite 了,它會提供熱更新,讓你改程式碼馬上看得到效果:
docker compose exec frontend yarn dev
第五步:驗收成果,來去逛逛!
現在,你的 SaaS 骨架應用已經在運行了囉!
- 你的網站前台 (通常是登入註冊那些):
http://localhost:5173
(開發模式下) - 後端 API 的大門口:
http://localhost/api
- API 的說明書 (Swagger UI):
http://localhost/api/documentation
- 測試信件的收件夾 (MailHog):
http://localhost:8025
登入看看,用 admin@example.com
這個帳號,去探索一下管理員才能進的 /admin/users
和 /admin/plans
頁面吧!
💡 程式碼解密:這些核心功能,它到底怎麼辦到的?
這個樣板不只是讓你跑起來,更希望你懂它。這裡挑幾個關鍵的程式碼片段,讓你看看它是怎麼「生」出這些功能的。
1. 後端權限誰說了算?Laravel Gate 來把關
Laravel 有個很實用的「Gate」機制,就像一個個的門,讓你決定誰能進、誰不能進。在 backend/app/Providers/AuthServiceProvider.php
裡,我們就設了兩個 Gate,專門給管理員用的:
// backend/app/Providers/AuthServiceProvider.php (部分程式碼)
use Illuminate\Support\Facades\Gate;
use App\Models\User;
public function boot(): void
{
// ... 其他權限設定
// 設一個 Gate 叫 'manage-users',只有 isAdmin() 是 true 的用戶才能過
Gate::define('manage-users', function (User $user) {
return $user->isAdmin();
});
// 'manage-plans' 這個 Gate 也一樣
Gate::define('manage-plans', function (User $user) {
return $user->isAdmin();
});
}
這些 Gate 怎麼用呢?很簡單,在 backend/routes/api.php
設定路由時,把它們掛上去就好了:
// backend/routes/api.php (部分程式碼)
use App\Http\Controllers\Api\Admin\{UserController, PlanController};
use App\Http\Controllers\Api\Auth\AuthController;
use Illuminate\Support\Facades\Route;
// 大家都可以用的路徑:註冊、登入
Route::post('register', [AuthController::class, 'register']);
Route::post('login', [AuthController::class, 'login']);
// 這些路徑需要先登入才能用
Route::middleware('auth:sanctum')->group(function () {
Route::post('logout', [AuthController::class, 'logout']);
Route::get('user', [AuthController::class, 'me']);
// 特別為管理員開的「後門」
Route::prefix('admin')->group(function () {
// 要有 'manage-users' 權限才能管用戶資料
Route::middleware('can:manage-users')->group(function () {
Route::apiResource('users', UserController::class);
});
// 要有 'manage-plans' 權限才能管方案資料
Route::middleware('can:manage-plans')->group(function () {
Route::apiResource('plans', PlanController::class);
});
});
});
這樣一來,不該看到的東西、不該動到的功能,一般用戶就碰不到啦,很安全!
2. 前台用戶狀態怎麼管?Pinia Store 讓你省心!
Vue 3 搭配 Pinia,管理前端的狀態就變得超直覺。在 frontend/src/stores/auth.js
裡,我們把用戶的登入狀態、Token、是不是管理員,都放在這裡集中管理:
// frontend/src/stores/auth.js (部分程式碼)
import { defineStore } from 'pinia';
import api from '@/api'; // 我們包好的那個 Axios
export const useAuthStore = defineStore('auth', {
state: () => ({
user: JSON.parse(localStorage.getItem('user')) || null, // 從瀏覽器儲存區撈用戶資料
token: localStorage.getItem('token')) || null, // 從瀏覽器儲存區撈登入憑證
}),
getters: {
isLoggedIn: (state) => !!state.token, // 有 token 就當作登入
isAdmin: (state) => state.user && state.user.is_admin, // 看用戶資料裡是不是管理員
},
actions: {
async login(credentials) {
try {
const response = await api.post('/login', credentials);
const { token, user } = response.data; // 登入成功後,後端會回傳這些
this.token = token;
this.user = user;
localStorage.setItem('token', token); // 把 token 存起來
localStorage.setItem('user', JSON.stringify(user)); // 把用戶資料也存起來
api.defaults.headers.common['Authorization'] = `Bearer ${token}`; // 設定之後每次發送請求都自動帶 token
return true;
} catch (error) {
throw error; // 有錯就丟出去給別人處理
}
},
async logout() {
try {
if (this.isLoggedIn) {
await api.post('/logout'); // 告訴後端,這個 token 我不要了
}
} finally {
this.token = null;
this.user = null;
localStorage.removeItem('token'); // 本地資料清一清
localStorage.removeItem('user');
delete api.defaults.headers.common['Authorization']; // Axios 也把 token 拿掉
}
},
// ... fetchUser() 和 register() 等其他 action
},
});
這樣一來,任何前端頁面想知道「現在是不是登入狀態?」、「這是不是管理員啊?」只要問 authStore
就行了,超方便!
3. 前端頁面誰能看?Vue Router 守衛來幫忙!
光後端有權限還不夠,前端頁面也要跟著管。frontend/src/router/index.js
裡面的 Vue Router 守衛,就是負責這塊的:
// frontend/src/router/index.js (部分程式碼)
import { createRouter, createWebHistory } from 'vue-router';
import { useAuthStore } from '@/stores/auth'; // 抓我們的 authStore
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
// 大家都能看的路徑
{ path: '/', name: 'home', component: () => import('../views/HomeView.vue') },
{ path: '/login', name: 'login', component: () => import('../views/auth/LoginView.vue') },
{ path: '/register', name: 'register', component: () => import('../views/auth/RegisterView.vue') },
// 需要登入才能看的路徑
{
path: '/dashboard',
name: 'dashboard',
component: () => import('../views/DashboardView.vue'),
meta: { requiresAuth: true }, // 設定這個路徑需要認證
},
// 管理員才能看、才能用的路徑群組
{
path: '/admin',
name: 'admin',
meta: { requiresAuth: true, requiresAdmin: true }, // 需要認證,而且還要管理員身份
children: [
{ path: 'users', name: 'admin-users-list', component: () => import('../views/admin/UserListView.vue') },
{ path: 'users/create', name: 'admin-users-create', component: () => import('../views/admin/UserFormView.vue') },
{ path: 'plans', name: 'admin-plans-list', component: () => import('../views/admin/PlanListView.vue') },
{ path: 'plans/create', name: 'admin-plans-create', component: () => import('../views/admin/PlanFormView.vue') },
// ... 其他管理員頁面
],
},
// ... 其他路徑
],
});
// 每次頁面跳轉前,這個守衛都會檢查
router.beforeEach(async (to, from, next) => {
const authStore = useAuthStore();
// 如果有登入憑證 (token),但還沒抓到用戶資料,就趕快去抓一下,避免頁面重新整理後狀態不見
if (authStore.token && !authStore.user) {
try {
await authStore.fetchUser();
} catch (error) {
console.error('抓取用戶資料失敗:', error);
authStore.logout(); // 如果 token 已經失效,就直接登出
return next({ name: 'login' }); // 導回登入頁
}
}
const requiresAuth = to.meta.requiresAuth; // 這頁需不需要登入
const requiresAdmin = to.meta.requiresAdmin; // 這頁需不需要管理員權限
if (requiresAuth && !authStore.isLoggedIn) {
// 如果要登入才能看,結果沒登入,就踢到登入頁
next({ name: 'login' });
} else if (requiresAdmin && !authStore.isAdmin) {
// 如果要管理員才能看,結果不是管理員,就踢到儀表板
next({ name: 'dashboard' });
} else {
next(); // 沒問題,放行!
}
});
export default router;
有了這個,就可以避免一般用戶亂闖管理員的頁面,或沒登入就想看 Dashboard,把頁面導引到正確的地方,讓使用者體驗更流暢。
4. 前端跟後端講話怎麼通?Axios 攔截器來幫忙!
前端要跟後端要資料,每次都要手動帶登入憑證嗎?不用啦!在 frontend/src/api/index.js
裡,我們用了 Axios 的「攔截器」,它會自動幫你處理這些事:
// frontend/src/api/index.js (部分程式碼)
import axios from 'axios';
import { useAuthStore } from '@/stores/auth';
import router from '@/router';
const api = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL || '/api', // 後端 API 的網址
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
});
// 請求攔截器:每次發送請求前,自動在訊息頭帶上 Authorization (登入憑證)
api.interceptors.request.use(config => {
const authStore = useAuthStore();
if (authStore.token) {
config.headers.Authorization = `Bearer ${authStore.token}`;
}
return config;
}, error => {
return Promise.reject(error);
});
// 響應攔截器:接收到後端回覆時,檢查是不是有問題
api.interceptors.response.use(response => {
return response;
}, async error => {
const authStore = useAuthStore();
// 如果後端回傳 401 (沒權限),而且用戶當前還認為自己是登入狀態 (表示憑證可能過期了)
if (error.response && error.response.status === 401 && authStore.isLoggedIn) {
console.warn('喔喔!遇到 401 Unauthorized,可能登入憑證失效了,準備登出並回到登入頁。');
await authStore.logout(); // 執行登出流程
router.push({ name: 'login' }); // 自動跳到登入頁面
}
return Promise.reject(error);
});
export default api;
這個攔截器真是個「好幫手」,它不只幫你自動帶憑證,還會在憑證失效的時候,貼心地把你登出,並導回登入頁,不用你自己手動寫一大堆判斷邏輯。
展望未來,還有很多可以做的啦!
這個 SaaS Boilerplate 就像是一棟剛蓋好的透天厝毛胚屋,基礎很穩,但裡面還有很多空間可以讓你發揮。你可以繼續:
- 把金流串起來:接上 Stripe 或 PayPal,讓你的訂閱功能能真的收錢錢。
- 搞多租戶功能:如果你的服務是要給不同公司或團隊用的,可以研究一下怎麼設計成多租戶架構。
- 更多報表跟管理工具:把你的 SaaS 搞得更專業、更實用。
- 持續優化:讓它跑得更快、更穩定。
希望這個樣板能成為你開發 SaaS 產品的「神隊友」,讓你的好點子能夠快速落地生根。如果你對這個專案有任何問題、建議,或是想幫忙貢獻,都歡迎到 GitHub 上來交流喔!
GitHub 專案連結:https://github.com/BpsEason/saas-boilerplate
沒有留言:
張貼留言