2025年7月26日 星期六

🚀 打造高效開發環境:Vue3 + Laravel 11 + gRPC 整合實戰

🚀 打造高效開發環境:Vue3 + Laravel 11 + gRPC 整合實戰

在現代 Web 開發中,快速迭代和高效協作是成功的關鍵。今天,我想與大家分享一個我為此目標設計的全棧專案範本:Vue-Laravel-Archi。這個專案整合了 Laravel 11 作為強大的後端框架,Vue 3 作為靈活的前端介面,並巧妙地結合了 gRPC 服務進行圖片處理,所有這些都透過 Docker Compose 進行容器化管理,旨在提供一個高效、易於部署的內網或本地開發環境。

您可以透過以下 GitHub 連結檢閱本專案的原始碼:https://github.com/BpsEason/vue-laravel-archi.git

專案起源與目標

在許多內部工具或快速原型開發的場景中,我們往往需要一個能迅速搭建、具備基本功能且易於擴展的基石。傳統的 RESTful API 固然強大,但在某些特定場景(如微服務間的高效通訊)下,gRPC 憑藉其基於 HTTP/2 的高效能和 Protocol Buffers 的序列化機制,展現出獨特的優勢。因此,我構思了這個專案,希望能:

  1. 提供一個基於 Laravel 11 和 Vue 3 的現代全棧應用範本。

  2. 展示如何將 gRPC 服務整合到傳統的 Web 應用中,特別是處理計算密集型任務,如圖片處理。

  3. 利用 Docker Compose 簡化多服務部署和環境配置。

  4. 專注於開發環境,提供快速啟動、易於除錯的體驗。

專案架構概覽

Vue-Laravel-Archi 的核心設計理念是模組化和高內聚低耦合。以下是其主要組成部分:

  • 後端 (Laravel 11)

    • 提供 RESTful API 介面,支援 JWT 認證,確保 API 安全。

    • 整合 Swagger (OpenAPI) 文件,方便 API 測試與協作。

    • 利用 Redis 進行高效的快取和 Session 管理。

    • 核心特色:透過 RoadRunner 運行一個獨立的 gRPC Image Worker 服務,專門負責圖片的 WebP 轉換和儲存,將繁重的圖片處理任務從主應用分離。

  • 前端 (Vue 3 + Pinia)

    • 基於 Vue 3、Pinia 和 Vue Router 構建的管理介面。

    • 編譯後的靜態檔案由 Nginx 提供服務,實現高效的靜態資源訪問。

  • 資料庫 (MySQL 8.0)

    • 使用 MySQL 8.0 作為主要資料儲存,包含預設的 users 表和測試用戶,方便快速啟動。

  • 快取與 Session (Redis 7)

    • 採用 Redis 7 (Alpine 版本),並啟用 AOF 持久化,確保資料不丟失。

  • 容器化 (Docker Compose)

    • 統一管理 frontend (Vue 構建)、backend (Laravel + Nginx)、mysqlredisimage-worker (gRPC) 五個服務。

    • 使用共享卷 (Shared Volumes) frontend_distbackend_storage_public,實現前端靜態檔案和圖片檔案的共享,避免重複構建和同步問題。

  • Nginx 配置

    • 跑在 backend 容器內,不僅負責靜態檔案的提供,也作為 API 的反向代理。

    • 針對圖片檔案設定長效快取,優化前端載入效能。

為什麼選擇 gRPC 進行圖片處理?

Vue-Laravel-Archi 中,圖片處理是一個獨立的 gRPC 服務,這帶來了幾個顯著的優勢:

  • 效能優化:圖片轉換通常是計算密集型操作。將其分離到一個獨立的 gRPC 服務中,可以避免阻塞主應用進程,提高 API 響應速度。

  • 技術棧解耦:圖片處理服務可以使用任何語言實現(例如:PHP, Go, Python, Node.js),只要它支援 gRPC。這使得未來可以根據效能或特定函式庫的需求,靈活選擇最適合的技術棧。

  • 資源隔離:可以獨立擴展圖片處理服務的資源,而不會影響到 Web 服務的穩定性。

  • 跨語言通訊:gRPC 的跨語言特性使得不同服務之間(即使使用不同的語言編寫)的通訊變得無縫。

在這個專案中,我們使用 RoadRunner 作為 PHP gRPC 服務的執行環境,並結合 Intervention/Image 庫來實現圖片的 WebP 轉換。

如何快速啟動專案?

這個專案的設計目標就是快速啟動和開發。以下是基本的步驟:

  1. 克隆專案

    Bash
    git clone https://github.com/BpsEason/vue-laravel-archi.git
    cd vue-laravel-archi
    

    【重要提示】:

    本專案的 GitHub 倉庫 僅包含關鍵程式碼檔案和架構目錄,不包含完整的 Laravel 和 Vue 核心依賴。這是為了保持倉庫的輕量化,並確保您可以從最新版本的依賴開始構建。因此,您需要自行完成相關框架的初始化。

    如果您的專案根目錄 (vue-laravel-archi/) 下的 backend/frontend-admin/ 目錄是空的(或只包含少量核心檔案),請按照以下步驟手動初始化 Laravel 和 Vue 專案:

    • 初始化 Laravel 後端 (在 backend 目錄內)

      Bash
      # 進入 backend 目錄
      cd backend
      # 如果你沒有 Laravel 安裝器,可以透過 Composer 建立一個新專案 (會自動安裝依賴)
      composer create-project laravel/laravel . --prefer-dist
      # 如果你已經有 Laravel 安裝器,可以直接執行
      # laravel new .
      # 回到專案根目錄
      cd ..
      

      注意:執行上述命令後,您需要將 GitHub 倉庫中的 /backend 目錄下的關鍵檔案和新增的服務程式碼(例如 app/Services/ImageService.php, app/Http/Controllers/Api/Admin/ImageController.php 等)複製到您新創建的 Laravel 專案中,覆蓋或合併相應的檔案。特別是 app/GrpcStubs 目錄下的文件,這些是 gRPC stubs,確保它們存在。

    • 初始化 Vue 前端 (在 frontend-admin 目錄內)

      Bash
      # 進入 frontend-admin 目錄
      cd frontend-admin
      # 創建一個新的 Vue 專案 (選擇 Vue 3, Pinia, Vue Router 等選項)
      npm create vue@latest .
      # 或 yarn create vue@latest .
      # 安裝依賴
      npm install # 或 yarn install
      # 回到專案根目錄
      cd ..
      

      注意:同樣地,您需要將 GitHub 倉庫中的 /frontend-admin 目錄下的關鍵 Vue 組件、Pinia Store、工具類等程式碼複製到您新創建的 Vue 專案中,覆蓋或合併相應的檔案。

  2. 環境準備 (Docker):

    確保您的系統已安裝 Docker 和 Docker Compose。

  3. 構建與啟動

    Bash
    docker-compose up --build -d
    

    這個命令會自動構建所有服務的 Docker 映像,並在後台啟動它們。

    它還會自動執行 Laravel 的初始化命令 (composer install, key:generate, jwt:secret, migrate)。

    【特別說明】:在 docker-compose.yml 中的 backend 服務的 command 裡包含了 composer install 和 php artisan migrate 等步驟。這些命令會在容器第一次啟動時執行,處理 Laravel 應用所需的依賴和資料庫初始化。

  4. 訪問應用

    • 前端:訪問 http://localhost,使用 admin@example.com 和預設密碼登入後台。

    • API:管理員端點位於 /api/admin/* (例如 /api/admin/login, /api/admin/images/upload);客戶端端點位於 /api/client/*

    • 圖片上傳POST /api/admin/images/upload (需 JWT token)。

關鍵程式碼解析

以下是專案中的一些關鍵程式碼片段,附帶詳細註解,希望能幫助您理解其核心功能。

1. docker-compose.yml:服務編排核心

這個檔案定義了所有服務及其之間的關係、網路和共享卷,是整個專案的骨架。

YAML
# docker-compose.yml
version: '3.8'

services:
  # Laravel 後端和 Nginx 服務
  backend:
    build:
      context: .
      dockerfile: ./docker/php/Dockerfile
    container_name: vue-laravel-archi-backend
    volumes:
      - ./backend:/var/www/html/backend # 將本地 backend 目錄映射到容器內
      - frontend_dist:/var/www/html/frontend-admin/dist # 共享前端編譯結果
      - backend_storage_public:/var/www/html/backend/storage/app/public # 共享圖片儲存
    ports:
      - "80:80" # 映射 80 端口供外部訪問
    environment:
      # Laravel 環境變數
      APP_ENV: local
      APP_DEBUG: "true"
      DB_HOST: mysql
      DB_DATABASE: laravel_vue_db
      DB_USERNAME: laravel_user
      DB_PASSWORD: laravel_password
      REDIS_HOST: redis
      # gRPC 服務的位址和端口
      IMAGE_GRPC_HOST: image-worker
      IMAGE_GRPC_PORT: 50051
    depends_on:
      - mysql
      - redis
      - image-worker # 依賴 gRPC 圖片服務
    networks:
      - app-network
    # 啟動命令:在後台運行 PHP-FPM,並在啟動時執行 Laravel 初始化
    command: >
      sh -c "composer install --no-interaction &&
             php artisan key:generate --ansi &&
             php artisan jwt:secret --force &&
             php artisan migrate --force &&
             php-fpm"

  # Vue 前端編譯服務 (只用於構建,不常駐運行)
  frontend-build:
    build:
      context: .
      dockerfile: ./docker/frontend/Dockerfile
    container_name: vue-laravel-archi-frontend-build
    volumes:
      - ./frontend-admin:/app/frontend-admin # 映射前端原始碼
      - frontend_dist:/app/frontend-admin/dist # 將編譯結果輸出到共享卷
    networks:
      - app-network
    # 執行前端構建命令
    command: npm run build

  # MySQL 資料庫服務
  mysql:
    image: mysql:8.0
    container_name: vue-laravel-archi-mysql
    volumes:
      - mysql_data:/var/lib/mysql # 持久化資料
    environment:
      MYSQL_ROOT_PASSWORD: laravel_password_root
      MYSQL_DATABASE: laravel_vue_db
      MYSQL_USER: laravel_user
      MYSQL_PASSWORD: laravel_password
    ports:
      - "3306:3306"
    networks:
      - app-network

  # Redis 快取服務
  redis:
    image: redis:7-alpine
    container_name: vue-laravel-archi-redis
    volumes:
      - redis_data:/data # 持久化資料
    command: redis-server --appendonly yes # 啟用 AOF 持久化
    ports:
      - "6379:6379"
    networks:
      - app-network

  # gRPC 圖片處理服務 (RoadRunner)
  image-worker:
    build:
      context: .
      dockerfile: ./docker/grpc/Dockerfile
    container_name: vue-laravel-archi-image-worker
    volumes:
      - ./grpc-service:/app/grpc-service # 映射 gRPC 服務原始碼
      - backend_storage_public:/var/www/html/backend/storage/app/public # 共享圖片儲存路徑
    ports:
      - "50051:50051" # gRPC 服務端口
    networks:
      - app-network
    # RoadRunner 啟動命令
    command: /usr/local/bin/rr serve -c /app/grpc-service/.rr.yaml

# 定義共享卷,用於服務間的檔案共享和資料持久化
volumes:
  frontend_dist: {} # 用於前端靜態檔案
  backend_storage_public: {} # 用於後端儲存的公開圖片
  mysql_data: {} # MySQL 資料持久化
  redis_data: {} # Redis 資料持久化

networks:
  app-network:
    driver: bridge

說明

  • backend 服務:包含了 Laravel 後端和 Nginx。Nginx 配置在 docker/nginx/default.conf 中,負責將前端的靜態檔案 (frontend_dist 卷) 提供給用戶,並將 /api/ 路徑的請求代理到後端 PHP-FPM (127.0.0.1:9000)。

  • frontend-build 服務:只在啟動時執行 npm run build 來編譯 Vue 專案,將結果輸出到 frontend_dist 卷,然後容器退出。

  • image-worker 服務:獨立的 gRPC 服務,使用 RoadRunner 運行。它映射了 grpc-service 目錄和 backend_storage_public 卷,以便處理圖片並儲存到正確的位置。

2. 後端:ImageService.php (gRPC 客戶端)

這是 Laravel 後端調用 gRPC 圖片處理服務的關鍵邏輯。

PHP
// backend/app/Services/ImageService.php
<?php

namespace App\Services;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use App\GrpcStubs\ImageProcessor\ImageRequest; // 假設由 protoc 生成
use App\GrpcStubs\ImageProcessor\ImageProcessorClient; // 假設由 protoc 生成

class ImageService {
    protected $grpcClient;

    public function __construct()
    {
        $grpcHost = env('IMAGE_GRPC_HOST', 'image-worker');
        $grpcPort = env('IMAGE_GRPC_PORT', '50051');
        $serverAddress = sprintf('%s:%s', $grpcHost, $grpcPort);

        // 注意:這裡使用 createInsecure() 是因為沒有 HTTPS,僅用於內部 Docker 網路通訊
        $this->grpcClient = new ImageProcessorClient($serverAddress, [
            'credentials' => \Grpc\ChannelCredentials::createInsecure(),
        ]);
    }

    /**
     * 將圖片上傳任務分發到 gRPC 服務。
     *
     * @param Request $request 包含上傳檔案的 HTTP 請求
     * @return string 返回圖片的公開 URL
     */
    public function processImageViaGrpc(Request $request): string {
        $request->validate(['image' => 'required|image|mimes:jpeg,png,jpg|max:2048']);
        $file = $request->file('image');

        $imageData = file_get_contents($file->getPathname());
        $filename = $file->getClientOriginalName();

        $grpcRequest = new ImageRequest();
        $grpcRequest->setImageData($imageData);
        $grpcRequest->setFilename($filename);

        try {
            // 調用 gRPC 服務的 ProcessImage 方法
            list($response, $status) = $this->grpcClient->ProcessImage($grpcRequest)->wait();

            if ($status->code === \Grpc\STATUS_OK) {
                return $response->getImageUrl();
            } else {
                Log::error("gRPC Image Processing Failed: " . $status->details, ['code' => $status->code]);
                throw new \Exception("圖片處理失敗: " . $status->details);
            }
        } catch (\Exception $e) {
            Log::error("Error calling gRPC Image Service: " . $e->getMessage());
            throw new \Exception("呼叫圖片處理服務時發生錯誤: " . $e->getMessage());
        }
    }
}

說明

  • ImageService 負責建立與 gRPC 圖片服務的連接。

  • 它從環境變數 (IMAGE_GRPC_HOST, IMAGE_GRPC_PORT) 獲取 gRPC 服務的位址。在 Docker Compose 環境中,image-worker 就是服務的名稱,會被 Docker 自動解析為其內部 IP。

  • processImageViaGrpc 方法接收上傳的圖片檔案,將其內容和檔案名封裝到 ImageRequest gRPC 消息中,然後發送請求給 ImageProcessorClient

  • 服務的回應 (ImageResponse) 包含了處理後的圖片 URL。

3. gRPC 服務:image.proto (Protocol Buffers 定義)

這是定義 gRPC 服務介面和消息格式的檔案,它是跨語言通訊的基礎。

Protocol Buffers
// grpc-service/proto/image.proto
syntax = "proto3";
package ImageProcessor; // 定義包名

// 服務定義
service ImageProcessor {
  // 定義一個處理圖片的 RPC 方法
  rpc ProcessImage (ImageRequest) returns (ImageResponse);
}

// 請求消息
message ImageRequest {
  bytes image_data = 1; // 圖片的二進制數據
  string filename = 2; // 原始文件名
}

// 響應消息
message ImageResponse {
  string image_url = 1; // 處理後圖片的 URL
}

說明

  • service ImageProcessor 定義了一個名為 ImageProcessor 的 gRPC 服務。

  • rpc ProcessImage 定義了一個遠端調用,它接收 ImageRequest 並返回 ImageResponse

  • message ImageRequestmessage ImageResponse 定義了請求和回應中包含的資料結構。bytes image_data 用於傳輸圖片的二進制數據,string image_url 用於返回圖片的公開 URL。

4. gRPC 服務:ImageProcessorImplementation.php

這是 gRPC 圖片處理服務的實際 PHP 實現。

PHP
// grpc-service/src/Services/ImageProcessorImplementation.php
<?php

namespace Grpc\ImageProcessor\Services;

use Grpc\ImageProcessor\ImageRequest;
use Grpc\ImageProcessor\ImageResponse;
use Grpc\ImageProcessor\ImageProcessorGrpcInterface; // 由 protoc 生成的介面
use Spiral\RoadRunner\GRPC\ContextInterface;
use Intervention\Image\Facades\Image; // 引入 Intervention/Image
use Illuminate\Support\Facades\Storage; // 引入 Storage facade
use Illuminate\Support\Str; // 引入 Str 輔助函數
use Illuminate\Support\Facades\Log; // 引入 Log

class ImageProcessorImplementation implements ImageProcessorGrpcInterface {
    /**
     * 處理圖片並返回 URL。
     *
     * @param ContextInterface $context
     * @param ImageRequest $request
     * @return ImageResponse
     */
    public function ProcessImage(ContextInterface $context, ImageRequest $request): ImageResponse {
        try {
            $imageData = $request->getImageData();
            $originalFilename = $request->getFilename();

            // 生成唯一文件名,確保不重複
            $filenameWithoutExt = pathinfo($originalFilename, PATHINFO_FILENAME);
            $newFilename = Str::slug($filenameWithoutExt) . '-' . time() . '-' . Str::random(8) . '.webp';

            // 使用 Intervention/Image 處理圖片
            $image = Image::make($imageData);
            // 調整圖片大小,限制寬度為 1200px,高度按比例
            $image->resize(1200, null, function ($constraint) {
                $constraint->aspectRatio();
                $constraint->upsize(); // 只有圖片大於 1200px 時才縮小
            });
            // 轉換為 WebP 格式,質量 80
            $webpContent = $image->encode('webp', 80);

            // 儲存到 Laravel 的 public 儲存空間 (透過共享卷實現)
            // 確保 backend_storage_public 卷被正確掛載
            Storage::disk('public')->put('images/' . $newFilename, $webpContent);

            $response = new ImageResponse();
            $response->setImageUrl(url('storage/images/' . $newFilename)); // 返回可訪問的 URL
            Log::info("Image processed and saved: " . $newFilename);

            return $response;
        } catch (\Exception $e) {
            Log::error("Error in gRPC ImageProcessor: " . $e->getMessage());
            // 如果發生錯誤,返回一個錯誤的響應 (或在 gRPC 層處理錯誤狀態)
            // 在 RoadRunner 中,通常透過抛出異常讓 RoadRunner 返回錯誤狀態
            throw new \RuntimeException("Image processing failed: " . $e->getMessage());
        }
    }
}

說明

  • 這個類別實現了由 image.proto 定義的 ImageProcessorGrpcInterface

  • ProcessImage 方法是實際處理圖片的邏輯所在。它接收二進制圖片數據和原始檔案名。

  • 利用 Intervention/Image 庫對圖片進行尺寸調整並轉換為 WebP 格式。

  • Storage::disk('public')->put() 將處理後的圖片儲存到 backend/storage/app/public 目錄,由於 docker-compose.yml 中配置了共享卷,這個目錄在 backend 容器和 image-worker 容器之間是共享的。

  • 最後,返回包含圖片公開 URL 的 ImageResponse

5. 前端:auth.js (JWT 認證與 Axios 攔截器)

Vue 前端使用 Pinia 進行狀態管理,並透過 Axios 攔截器自動處理 JWT token 的發送和刷新。

JavaScript
// frontend-admin/src/stores/auth.js
import { defineStore } from 'pinia';
import { ref } from 'vue';
import axios from 'axios';

export const useAuthStore = defineStore('auth', () => {
    const token = ref(localStorage.getItem('admin_token') || '');
    const user = ref(JSON.parse(localStorage.getItem('admin_user') || 'null'));
    const isRefreshing = ref(false); // 標記是否正在刷新 token
    let failedRequestsQueue = []; // 儲存因 token 過期而失敗的請求

    const axiosInstance = axios.create({
        baseURL: '/', // 所有請求都相對於根路徑
        headers: {
            'Content-Type': 'application/json',
            'Accept': 'application/json',
        },
    });

    // 請求攔截器:在每次請求前加上 JWT token
    axiosInstance.interceptors.request.use(
        config => {
            if (token.value && !config.headers.Authorization) {
                config.headers.Authorization = `Bearer ${token.value}`;
            }
            return config;
        },
        error => Promise.reject(error)
    );

    // 響應攔截器:處理 401 (Unauthorized) 錯誤,實現 token 刷新
    axiosInstance.interceptors.response.use(
        response => response,
        async error => {
            const originalRequest = error.config;

            // 檢查是否是 401 錯誤,且不是刷新 token 的請求,且未重試過
            if (error.response?.status === 401 && originalRequest.url.indexOf('/refresh') === -1 && !originalRequest._retry) {
                originalRequest._retry = true; // 標記為已重試

                if (!isRefreshing.value) { // 如果沒有其他請求正在刷新 token
                    isRefreshing.value = true; // 設置正在刷新標誌
                    try {
                        const refreshResponse = await axiosInstance.post('/api/admin/refresh'); // 發送刷新請求
                        token.value = refreshResponse.data.token; // 更新 token
                        localStorage.setItem('admin_token', token.value); // 儲存新 token
                        
                        // 處理所有排隊的失敗請求
                        failedRequestsQueue.forEach(promise => promise.resolve());
                        failedRequestsQueue = [];
                        isRefreshing.value = false; // 重置標誌

                        // 用新 token 重發原始請求
                        originalRequest.headers['Authorization'] = `Bearer ${token.value}`;
                        return axiosInstance(originalRequest);
                    } catch (refreshError) {
                        console.error('Token refresh failed:', refreshError.response?.data?.error || refreshError.message);
                        isRefreshing.value = false;
                        // 刷新失敗,拒絕所有排隊的請求
                        failedRequestsQueue.forEach(promise => promise.reject(refreshError));
                        failedRequestsQueue = [];
                        logout(); // 刷新失敗,登出用戶
                        return Promise.reject(refreshError);
                    }
                } else {
                    // 如果正在刷新,將當前請求加入隊列
                    return new Promise((resolve, reject) => {
                        failedRequestsQueue.push({ resolve, reject });
                    })
                    .then(() => {
                        originalRequest.headers['Authorization'] = `Bearer ${token.value}`;
                        return axiosInstance(originalRequest);
                    })
                    .catch(refreshError => {
                        return Promise.reject(refreshError);
                    });
                }
            }
            return Promise.reject(error);
        }
    );

    // 登入方法
    async function login(credentials) {
        try {
            const response = await axiosInstance.post('/api/admin/login', credentials);
            token.value = response.data.token;
            localStorage.setItem('admin_token', token.value);
            // 實際項目中,這裡會根據登入成功後的回應獲取用戶資訊
            console.log('Login successful');
            return true;
        } catch (error) {
            console.error('Login failed:', error.response?.data?.error || error.message);
            logout(); // 登入失敗則清除 token
            return false;
        }
    }

    // 登出方法
    function logout() {
        // 發送登出請求到後端,使 JWT 失效
        axiosInstance.post('/api/admin/logout').catch(e => console.error('Logout API failed:', e));
        token.value = '';
        user.value = null;
        localStorage.removeItem('admin_token');
        localStorage.removeItem('admin_user');
        console.log('Logged out');
    }

    // 暴露給組件使用的方法和狀態
    return { token, user, login, logout, axiosInstance };
});

說明

  • 使用 Pinia 的 defineStore 來定義認證 Store。

  • axiosInstance 配置了基礎 URL 和常用的 Header。

  • 請求攔截器:在每個發送的請求中自動附加 JWT Authorization Header,簡化了 API 調用。

  • 響應攔截器:這是處理 JWT 過期邏輯的核心。當接收到 401 錯誤時,它會判斷是否為 token 過期。如果是,則會嘗試發送 /api/admin/refresh 請求來獲取新的 token。為了避免多個請求同時觸發刷新,它使用 isRefreshing 標誌和 failedRequestsQueue 來對請求進行排隊,確保只有一個刷新操作在進行。

6. Nginx 配置:default.conf

這個檔案定義了 Nginx 在 backend 容器內如何處理流量。

Nginx
# docker/nginx/default.conf
server {
    listen 80; # 監聽 80 端口
    server_name localhost; # 伺服器名稱

    # 前端靜態檔案的路徑,來自 frontend_dist 共享卷
    root /var/www/html/frontend-admin/dist;
    index index.html index.htm;
    charset utf-8;

    # 所有非 /api/ 和 /storage/ 的請求都嘗試作為前端路由
    location / {
        try_files $uri $uri/ /index.html;
    }

    # /api/ 路徑代理到 Laravel 後端 PHP-FPM
    location /api/ {
        fastcgi_pass 127.0.0.1:9000; # PHP-FPM 服務
        fastcgi_index index.php;
        # 將請求路由到 Laravel 的 public/index.php
        fastcgi_param SCRIPT_FILENAME /var/www/html/backend/public/index.php;
        include fastcgi_params; # 包含常見的 FastCGI 參數
        fastcgi_param REQUEST_URI $request_uri;
        fastcgi_param PATH_INFO $fastcgi_path_info;
    }

    # /storage/ 路徑用於提供公開儲存的圖片
    location /storage/ {
        alias /var/www/html/backend/storage/app/public/; # 指向 Laravel 的公共儲存路徑
        try_files $uri $uri/ =404;
        # 對圖片設定長效快取
        add_header Cache-Control "public, max-age=31536000, immutable";
        add_header Access-Control-Allow-Origin "*"; # 允許跨域訪問
    }

    # 禁止訪問隱藏檔案 (點開頭的檔案)
    location ~ /\.(?!well-known).* {
        deny all;
    }
}

說明

  • Nginx 監聽 80 端口。

  • root /var/www/html/frontend-admin/dist; 將前端 Vue 編譯後的靜態檔案作為網站的根目錄。

  • location / 處理前端路由,確保單頁應用 (SPA) 刷新時能夠正確導航。

  • location /api/ 將所有以 /api/ 開頭的請求轉發給 Laravel 後端的 PHP-FPM 服務。

  • location /storage/ 用於直接提供由 Laravel 儲存的公開檔案(例如圖片),並設定了長效快取,以提升圖片載入效能。

注意事項 (僅限開發環境)

為了快速啟動和開發,這個專案有一些針對開發環境的假設和限制:

  1. 僅限開發環境,無 HTTPS:

    專案預設沒有配置 HTTPS。這表示所有數據都以明文傳輸,僅適合在安全的內部網路或本地環境中使用。切勿在生產環境直接部署! 生產環境需要額外配置 HTTPS (例如使用 Let’s Encrypt)。

  2. gRPC 配置:

    確保您的開發環境中正確安裝了 protoc (Protocol Buffers 編譯器) 和 grpc_php_plugin。這些工具用於從 .proto 文件生成 PHP 程式碼,是 gRPC 服務運行的基礎。專案的 vue-laravel-archi.sh 腳本中提供了相關提示。

  3. 日誌除錯:

    如果您遇到問題,請務必檢查容器日誌:

    Bash
    docker logs vue-laravel-archi-backend
    docker logs vue-laravel-archi-image-worker
    

    這些日誌會提供關鍵的除錯資訊。

結語

Vue-Laravel-Archi 專案為 Laravel 和 Vue 開發者提供了一個現代、高效且易於擴展的開發範本。透過整合 Docker Compose 和 gRPC,它不僅簡化了多服務應用的部署,還展示了如何在全棧應用中利用 gRPC 的優勢來處理特定任務。

我希望這個專案能為您的下一個內部工具或原型項目提供一個堅實的起點。歡迎大家嘗試並提出寶貴意見!


沒有留言:

張貼留言

熱門文章