顯示具有 Git 標籤的文章。 顯示所有文章
顯示具有 Git 標籤的文章。 顯示所有文章

2026年3月18日 星期三

🚀 拒絕當 CRUD 工程師!資深 PHP/Laravel 面試必考的硬核實戰筆記

 

1. PHP 7 核心特性、底層效能與語法陷阱

  • PHP 7 為什麼快?底層與核心優化:

    • ZVAL 結構重構: 變數容器從 24 降至 16 bytes,大幅降低內存占用並提升 CPU Cache 命中率。

    • AST (抽象語法樹): 引入 AST 作為編譯的中間層,讓編譯器與執行器解耦,提升編譯效率與語法解析的穩定性。

    • OPCache: 實務必開的效能神器。它會將 PHP 腳本編譯後的 Bytecode 快取在共享記憶體 (Shared Memory) 中,省去每次 Request 都要重新讀取、解析和編譯腳本的極大開銷。

  • 現代化語法與嚴謹度 (Robustness):

    • 嚴格類型 (Strict Types): 強烈建議在檔案開頭宣告 declare(strict_types=1);。這不只是為了效能,更是為確保傳入與回傳的資料型別完全一致,減少進入邏輯層前的低級錯誤(如字串 "10" 被當成整數 10 運算)。

    • Null 合併運算符 (??): 取代冗長的 isset() 三元運算,讓程式碼更優雅:$username = $_GET['user'] ?? 'Guest';。

    • 箭頭函式 (Arrow Functions): PHP 7.4 引入的 fn()。它會「自動以值傳遞 (By Value)」的方式捕捉父層作用域的變數,適合用在 array_map 等單行回呼處理:$multiplied = array_map(fn($n) => $n * $multiplier, $numbers);。

  • 閉包 (Closure) 的變數陷阱: 一般的匿名函式 function() use ($var) 預設是「值傳遞」。若要內外同步改變,務必加上 & 變為「引用傳遞」。

    PHP
    $count = 1;
    $func = function() use (&$count) { echo $count; }; // 加上 & 才是引用
    $count++;
    $func(); // 面試陷阱:若沒加 & 會輸出 1,加上 & 才會正確輸出 2
    

2. 軟體工程實踐與資安防護 (開發品質與防禦)

  • 軟體工程實踐 (Code Quality):

    • 規範與邏輯: 嚴格遵循 PSR-12 編碼規範。架構設計上落實 SOLID 原則 與 DRY (Don't Repeat Yourself),特別是單一職責原則 (SRP),確保一個 Controller 或 Service 只處理一件事,避免義大利麵條程式碼。

    • 測試覆蓋率: 推動 Unit Testing (PHPUnit)。在核心業務邏輯(如:折扣計算、金流處理、庫存扣減)必須要求 100% 的測試覆蓋率,確保重構時不會引發災難。

  • 安全性問題與資安意識:

    • SQL Injection: Laravel 的 Eloquent 底層使用 PDO Parameter Binding(參數綁定),預設已防禦 SQLi。但極度危險的陷阱在於使用 DB::raw() 時,務必手動進行參數綁定,絕對不可直接拼接變數。

    • XSS (跨站腳本攻擊): Blade 模板的 {{ $var }} 會自動執行 htmlspecialchars 轉義。若需輸出 HTML 才使用 {!! $var !!}(需確保資料來源絕對安全)。

    • CSRF (跨站請求偽造): Laravel 內建 VerifyCsrfToken 中間件,所有 POST/PUT 請求都必須攜帶合法的 Token。

    • 進階防禦: 實務上會透過 Nginx 或 Middleware 配置 Security Headers (如 HSTS 強制 HTTPS、Content-Security-Policy 防止外部惡意腳本載入);針對敏感個資 (如身分證、信用卡) 寫入資料庫前,嚴格使用 Laravel 的 Crypt facade 進行 AES-256-CBC 加密。

  • Laravel 中間件 (Middleware) 的妙用: 除了認證 (Auth),資深開發者常利用 Middleware 處理跨領域關注點 (Cross-Cutting Concerns):

    1. API 限流 (Rate Limiting): 搭配 Redis,針對特定 API (如登入、發送簡訊) 限制每分鐘請求次數,防止暴力破解或惡意洗流量。

    2. 日誌審計 (Log Audit): 攔截並記錄所有 POST/PUT/DELETE 的 Request Payload (請求內容) 與操作者 IP/ID,用於後續資安追蹤與行為稽核。

    3. 語系自動切換 (Localization): 根據前端 Header 帶上的 Accept-Language,在 Request 進入 Controller 前自動切換 App::setLocale() 全域語系。

3. 千萬級資料庫設計、優化與 Laravel 實戰架構

  • 資料庫設計與大數據量優化 (千萬級訂單表): 當 orders 表達到千萬等級,單純加索引已無法解決效能瓶頸,必須採取分層策略:

    1. 短線方案 (SQL 優化): 啟用慢查詢日誌 (Slow Query Log),使用 EXPLAIN 分析查詢語句。抓出索引失效的元凶(例如:欄位類型不匹配導致的隱式轉換、使用了 LIKE '%keyword' 導致全表掃描)。

    2. 中線方案 (冷熱分離): 將超過一年的歷史訂單移至 orders_archive 表。線上業務只查近一年的「熱資料」,大幅縮小 B+Tree 的深度與掃描範圍。

    3. 長線方案 (分庫分表 Sharding): 根據 user_id 取模(Hash Modulo,例如 user_id % 10)進行水平拆分,將資料分散到 10 張表或不同的資料庫實例,徹底減輕單機 I/O 壓力。

  • 千萬級資料下 Laravel Eloquent 的優化策略:

    • 解決 N+1 問題: 絕對使用 with() 進行 Eager Loading 預先載入關聯。但若關聯資料極大,改用 joins 或針對特定條件進行 Lazy Eager Loading (load()) 以節省記憶體。

    • 巨量資料處理 (chunk vs chunkById vs cursor):

      • 要更新 10 萬筆資料時,千萬別用 all() 撐爆記憶體。使用 chunk(1000) 分批取出。

      • 陷阱: 如果你在 chunk 迴圈中「更新了過濾條件的欄位」,會導致分頁偏移漏撈資料!這時必須改用基於主鍵的 chunkById()。

      • 極低記憶體消耗: 若只是要匯出報表或單純遍歷,使用 cursor()。它底層利用 PHP Generators (生成器),每次只在記憶體保留一個 Model 實例。

  • 讀寫分離與同步延遲 (Read/Write Splitting & Lag):

    • 實作配置: 在 config/database.php 中配置 read (多台 Slave IP 陣列) 與 write (單台 Master IP)。Laravel 底層會自動將 SELECT 隨機分配給 Slave,INSERT/UPDATE/DELETE 送給 Master,從而讓整體 QPS 呈倍數增長。

    • 主從延遲痛點 (Replication Lag): Master 寫入後,同步到 Slave 需要幾十到幾百毫秒。若用戶剛寫入馬上讀取,可能會因為查到還沒同步的 Slave 而看到舊資料。

    • 解法 1:開啟 Sticky (黏滯性):在配置中加上 'sticky' => true。原理是 Laravel 會在同一個 Request 週期內,只要執行過任何寫入,後續所有的讀取都會被強制導向 Master,完美解決「即寫即讀」的空值問題。

    • 解法 2:強制走主庫:如果是跨 Request 的高一致性需求(如結帳後跳轉看餘額),在查詢時明確使用 onWriteConnection():$user = User::onWriteConnection()->find($id);。

  • 高併發下的防超賣 (樂觀鎖 vs 悲觀鎖):

    • 悲觀鎖 (SELECT ... FOR UPDATE): 預設一定會有人搶。查詢當下利用 InnoDB 行鎖 (Row Lock) 鎖死資料,直到 Transaction 結束才釋放。其他 Request 會被強制 Block 等待。優點是絕對不會超賣,缺點是並發量高時會導致連線排隊甚至死鎖。

    • PHP
      DB::transaction(function () use ($productId, $buyCount) {
          // lockForUpdate() 會在底層產生 SELECT * FROM products WHERE id = ? FOR UPDATE
          $product = Product::where('id', $productId)->lockForUpdate()->first();
      
          if ($product->stock >= $buyCount) {
              $product->stock -= $buyCount;
              $product->save();
              // 執行到這裡,Transaction 結束,鎖定自動釋放
          } else {
              throw new Exception('庫存不足');
          }
      });
      

    • 樂觀鎖: 適合讀多寫少。在資料表加一個 version 欄位。更新時檢查版本號是否與讀取時一致。如果更新筆數為 0,代表被別人搶先,可選擇報錯或重試。

      PHP
      $affectedRows = DB::table('products')->where('id', 1)->where('version', $oldVersion)
          ->update(['stock' => $stock - 1, 'version' => $oldVersion + 1]);
      
  • 最終殺器:引入 Elasticsearch (ES) 處理複雜搜尋

    • 為什麼要引入 ES? MySQL 的 B+Tree 索引在處理「多條件組合過濾(如電商的價格區間+品牌+多標籤)」或「模糊搜尋 (LIKE %keyword%)」時極易失效,導致全表掃描。ES 使用「倒排索引 (Inverted Index)」,能達到毫秒級的全文檢索與複雜聚合分析,徹底解放 MySQL 的 CPU 與 I/O 壓力。

    • 怎麼引入?

      1. 套件選擇: 在 Laravel 中,通常透過 Laravel Scout 搭配 ES Driver,或直接使用官方的 elasticsearch/elasticsearch 套件。

      2. 資料同步 (Data Sync): MySQL 依然是 Source of Truth。同步資料通常採用兩種方式:

        • 異步隊列 (Queue): 在 Model 的 saved / deleted 事件中,把資料丟進 RabbitMQ 或 Laravel Queue,再由 Worker 寫入 ES。

        • Binlog 訂閱 (進階): 使用 Canal 或 Maxwell 監聽 MySQL 的 Binlog,實現與業務代碼完全解耦的即時同步。

    • 實作範例 (Laravel Scout):

      PHP
      // 在 Model 中引入 Searchable
      class Order extends Model {
          use Searchable;
      
          // 定義要寫入 ES 的資料結構
          public function toSearchableArray() {
              return [
                  'id' => $this->id,
                  'user_id' => $this->user_id,
                  'status' => $this->status,
                  'product_names' => $this->products->pluck('name')->implode(','),
              ];
          }
      }
      
      // 複雜搜尋時,直接將壓力轉移給 ES
      $orders = Order::search('iPhone 15')->where('status', 'paid')->get();
      

4. 高併發架構、Redis 深度應用與常駐記憶體 (Swoole)

  • 每秒萬次請求 (10k TPS) 的解耦與快取架構: 面對瞬間湧入的秒殺或搶票流量,不能讓請求直接打進 MySQL。

    1. 限流攔截: 最外層 Nginx / API 閘道先擋掉惡意請求。

    2. Redis 預扣庫存: 將庫存提早載入 Redis,利用單執行緒原子的 DECR 指令進行高併發扣減,MySQL 完全不參與此階段。

    3. 非同步解耦 (Message Queue): 扣減成功的 Request 不會立刻同步寫入 DB,而是將「訂單建立任務」丟入 RabbitMQ 或 Kafka。

    4. Worker 削峰填谷: 後台的 Consumer (Worker) 依照資料庫能承受的速度,平穩地將隊列中的訂單寫入 MySQL。

  • Redis 實戰場景與三大陷阱防禦:

      1. 快取穿透 (Cache Penetration):

        • 定義: 駭客惡意頻繁查詢一個「快取與資料庫都不存在」的 Key(例如 id = -1)。因為快取查不到,請求全部「穿透」打向 MySQL。

        • 解法: 把 null 值也存進快取(設定極短的過期時間如 60 秒),或在最前端使用布隆過濾器 (Bloom Filter) 直接攔截不存在的 Key。

        PHP
        $user = Cache::remember("user:{$id}", 60, function () use ($id) {
            $data = User::find($id);
            return $data ?: 'null_placeholder'; // 找不到也快取起來,防穿透
        });
        
      2. 快取擊穿 (Cache Breakdown / Hotspot Invalid):

        • 定義: 一個極度熱門的 Key(例如:五月天演唱會售票資訊),在過期失效的「那一瞬間」,剛好有上萬個併發請求湧入。發現快取沒資料,這上萬個請求會同時衝向資料庫去重建快取,瞬間壓垮 DB。

        • 解法: 使用互斥鎖 (Mutex Lock)。發現快取失效時,只有「第一個」拿到鎖的請求可以去查 DB 並重建快取,其他請求必須等待重試或返回舊資料。

        PHP
        $data = Cache::get($key);
        if (!$data) {
            // 嘗試獲取一個 5 秒自動過期的鎖
            $lock = Cache::lock("lock:{$key}", 5);
            if ($lock->get()) {
                // 只有拿到鎖的這單一 Request 能查 DB
                $data = DB::table('hot_data')->first();
                Cache::put($key, $data, 3600);
                $lock->release(); // 釋放鎖
            } else {
                // 沒拿到鎖的,休眠 50ms 後重試,或直接報錯/返回預設值
                usleep(50000); 
                return $this->getData(); 
            }
        }
        
      3. 快取雪崩 (Cache Avalanche):

        • 定義: 大量的 Key 在「同一時間」過期失效,或者 Redis 伺服器直接掛掉。導致原本由快取擋下的海量查詢,像雪崩一樣全部砸在 MySQL 上。

        • 解法: 設定過期時間時,務必加上「隨機擾動值 (Jitter)」將失效時間打散;架構上確保 Redis 的高可用性(Sentinel 或 Cluster)。

        PHP
        // 基礎 60 分鐘 + 隨機 1~300 秒,避免大量 Key 在同一秒集體失效
        Cache::put('key', $data, now()->addMinutes(60)->addSeconds(rand(1, 300)));
        
    • Redis 的其他強大應用:

      1. Atomic Locks (分散式鎖): 利用 SETNX (Laravel 中的 Cache::lock()) 防止同一秒內用戶連按按鈕導致的重複發放優惠券或重複扣款。

      2. Rate Limiting (API 限流): 利用 Redis 的 Sorted Set 或簡單計數器實作令牌桶/漏桶演算法,控制每分鐘 API 呼叫上限。

      3. Pub/Sub (發布/訂閱): 用於輕量級的即時訊息推播系統(如聊天室廣播、系統即時通知),比傳統輪詢 (Polling) 更省資源。

  • WebSocket 長連接:穩定性與橫向擴展 (Horizontal Scaling):

    • 單機 Swoole/Workerman 處理 WebSocket 連線數有上限。當需要橫向擴展部署多台伺服器時,會遇到「廣播難題」:A 用戶連在 Server 1,B 用戶連在 Server 2,Server 1 收到 A 的訊息,要怎麼傳給 B?

    • 解法:引入 Redis Pub/Sub 作為內部通訊橋樑。 當 Server 1 收到訊息時,不直接廣播,而是 Publish 到 Redis 頻道。所有 Server (包含 Server 2) 都 Subscribe 該頻道,收到 Redis 推播後,再各自找出維持在自己記憶體內的 WebSocket 連線 (FD) 並發送出去。

    • 穩定性 (心跳機制 Heartbeat): 必須實作 Ping/Pong 心跳包機制。由客戶端定期發送 Ping,伺服器若超時未收到,主動斷開該連線 (Close FD),避免「幽靈連線」耗盡伺服器資源。

  • Swoole 常駐記憶體與 max_request 治理: Swoole 採用 Event Loop 常駐記憶體,效能極高,但也帶來了致命的 Memory Leak (記憶體洩漏) 危機。因為在傳統 FPM 中,Request 結束後全域變數會被銷毀;但在 Swoole 中,如果你的 Singleton 或靜態陣列 (static $cache = []) 持續塞入資料且未 unset(),記憶體會無限膨脹直到 OOM (Out of Memory)。

    • 治理方案 (安全墊配置):

      PHP
      $server->set([
          'max_request' => 1000, 
      ]);
      
    • 設定原因: 既然無法保證龐大的第三方擴展或團隊寫的每一行程式碼都 100% 不會 Leak,透過設定 max_request,強迫 Worker 進程在處理完 1000 次 Request 後「主動平滑重啟 (Graceful Restart)」。這能徹底銷毀並釋放該進程佔用的所有記憶體與殘留垃圾,是常駐記憶體環境中最有效的保底防禦手段。

    • 檢測工具: 開發階段可運用 gc_status() 觀察引用計數垃圾,或在 Request 前後調用 memory_get_usage() 計算差值;線上排查則依賴 Swoole Tracker 等分析工具 Dump 出記憶體快照抓出未釋放的物件。

    • memory_get_usage() 監控記憶體差值: 最簡單粗暴的方式。在 Worker 進程處理 Request 的開頭與結尾,分別紀錄記憶體消耗。如果 diff 數字隨著請求次數不斷上升且不回落,100% 有 Leak。

      PHP
      $server->on('request', function ($request, $response) {
          $startMemory = memory_get_usage();
      
          // ... 執行業務邏輯 ...
      
          $endMemory = memory_get_usage();
          $diff = $endMemory - $startMemory;
          if ($diff > 1024 * 500) { // 單次請求殘留超過 500KB 就發出告警
              Log::warning("Potential Memory Leak! Leaked: {$diff} bytes.");
          }
      });
      
    • gc_status() 分析垃圾回收池: PHP 的 GC 機制是基於引用計數。如果產生了「循環引用」或被全域變數咬住,物件就不會被銷毀。透過呼叫 gc_status() 可以觀察 roots 的數量。如果在壓力測試下,roots 數量無止盡地瘋狂增長,代表產生了大量無法回收的垃圾。

    • Swoole Tracker / Xhprof (專業工具抓兇手): 當確定有 Leak 後,要在幾萬行代碼中找到是哪一個變數沒釋放,人力很難辦到。這時必須在測試環境掛載 Swoole Tracker(官方商業工具)或 Xhprof 擴展。

      • 作法: 開啟 Memory Leak 檢測模式,發送壓測請求。工具會自動 Dump 出記憶體快照 (Memory Profiling),並在視覺化介面中明確告訴你:「是 App\Services\OrderService 裡面的 $staticCache 陣列佔據了 80% 的記憶體且未被釋放」,工程師只需精準去該處加上 unset() 即可解決。

5. 伺服器極限排錯與 Git 救火指南

  • API 變慢與 CPU 100% 排查流程:

    1. 輸入 top 看 Load Average 與 CPU 佔用排行。

    2. ps aux --sort=-%cpu | head -n 5 精準抓出最操 CPU 的進程 PID。

    3. 若為 PHP-FPM 進程,用 strace -p <PID> 觀察系統調用,看是否卡在某個 Socket 網路請求或 I/O 讀取。

    4. 調閱 PHP-FPM 的 slowlog,這裡會直接印出執行時間超標的 PHP 函式堆疊 (Stack Trace),直接定位到出問題的原始碼行數。

  • 磁碟空間 100% 爆滿急救陷阱: 當伺服器因 Nginx 或 Laravel 專案的 Log 塞滿導致完全卡死時,千萬不可以直接使用 rm -rf laravel.log!因為若 PHP-FPM 等進程還咬著該檔案的 File Descriptor,實體磁碟空間並不會被釋放。

    • 正確清空方式: 使用 > /var/log/laravel.log 或 cat /dev/null > /var/log/laravel.log。這能瞬間將檔案大小歸零,且不破壞檔案的讀寫關聯。

    • 若已誤刪且空間沒回來:執行 lsof | grep deleted 找出殭屍進程,重啟該進程服務即可釋放。

  • Git 進階協作與機密撤回:

    • 線上緊急 Bug (Hotfix): 正在開發 Feature 時突發 Bug,立刻 git stash 暫存手邊工作。切回 main 分支拉出 hotfix 分支。修復並測試後,合併回 main (使用 merge --no-ff 保留清晰的修復節點),推上線後,再回到 Feature 分支 git stash pop 繼續開發。

    • 重大資安:誤 Push .env 到 GitHub:

      1. 第一步:立刻更改所有外洩的真實密碼、API Keys。

      2. 使用 git filter-branch 或 BFG Repo-Cleaner 將該檔案從所有歷史 Commit 中徹底抹除。

        Bash
        git filter-branch --force --index-filter 'git rm --cached --ignore-unmatch .env' --prune-empty --tag-name-filter cat -- --all
        
      3. 將 .env 加入 .gitignore。

      4. 執行強制推送 git push origin --force --all 覆蓋遠端紀錄。

2024年7月24日 星期三

flutter第五彈,最後一哩路,v2ray專案,加上生命週期,遞迴bug修正,程式碼優化

聲明:本篇網誌沒有任何人被罵或受傷,請勿獵巫

正文開始

那個無限遞迴重整果然被開bug了,然後pm果然希望flutter背景執行要把vpn斷開,再來我也想把程式整理一下,我們開始以下故事吧

首先要監測app是否被移到背景執行需要有lifecycle,但flutter好像沒有相應pause的方法可以複寫,上網查好像也查不太到資料,幸好大神水哥又上演了神助攻

他推薦我一個plugin,看圖片就知道是他本人寫的,雖然沒上到官方hub,但以他技術應該是無虞,就直接git引用

# pubspec.yaml
dependencies:
...
mx_lifecycle:
git:
url: git@github.com:MagicalWater/mx_lifecycle.git

然後正常情況應該是會報 git@github.com: Permission denied

不要急,水哥繼續說,git套件安裝要走ssh,所以必須有ssh key,新增方法在這裡,產生pub key的指令就上網查,我就不贅述自己去看,完成就可以順利安裝完畢,至於使用範例,等等會在程式碼中

接著就上我們主菜吧

首先把V2ray連線拆成service

// lib/v2ray_service.dart
import 'package:flutter_v2ray/flutter_v2ray.dart';
import 'package:flutter/material.dart';

class V2rayService {
late final V2RayURL parser;
late final FlutterV2ray connection;
var status = ValueNotifier<V2RayStatus>(V2RayStatus());
var prevStatus = ValueNotifier<V2RayStatus>(V2RayStatus());
var hasReload = ValueNotifier<bool>(false);
bool get isConnected => status.value.state == "CONNECTED";

V2rayService(String connectText, Function updateView) {
parser = FlutterV2ray.parseFromURL(connectText);
connection = FlutterV2ray(
onStatusChanged: (_status) {
status.value = _status;
        // 觀察狀態變化
if (status.value != prevStatus.value) {
prevStatus.value = status.value;
          // 把可重整的值還原
hasReload.value = false;
}
updateView();
print("my status: ${status.value.state}");
},
);
}

init() {
return connection.initializeV2Ray();
}

connect() async {
if (await connection.requestPermission() && !isConnected) {
return connection.startV2Ray(
remark: parser.remark, config: parser.getFullConfiguration());
}
}

disconnect() {
if (isConnected) {
connection.stopV2Ray();
}
}

getVersion() {
return connection.getCoreVersion();
}
}

然後是主程式

// lib/main.dart
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
import 'package:mx_lifecycle/mx_lifecycle.dart';
import "./v2ray_service.dart";

void main() {
runApp(const MyApp());
}

class MyApp extends StatelessWidget {
const MyApp({super.key});

@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter V2Ray',
theme: ThemeData(
useMaterial3: true,
brightness: Brightness.dark,
inputDecorationTheme: const InputDecorationTheme(
border: OutlineInputBorder(),
),
),
home: SafeArea(
top: true,
right: true,
bottom: true,
left: true,
child: V2rayWebPage(),
),
);
}
}

class V2rayWebPage extends StatefulWidget {
const V2rayWebPage({super.key});

@override
State<V2rayWebPage> createState() => V2rayWebPageState();
}

class V2rayWebPageState extends State<V2rayWebPage> {
late final V2rayService v2rayService = new V2rayService(
"vmess://......",
() => setState(() {}));

late InAppWebViewController webViewController;
String url = "https://www.xxx.com/";

@override
void initState() {
super.initState();
v2rayService.init();
Future.delayed(const Duration(milliseconds: 500), () async {
print("version: ${await v2rayService.getVersion()}");
v2rayService.connect();
});
listenLifecycle();
}

// 水哥套件,lifecycle監聽
void listenLifecycle() {
final lifecycle = MxLifecycle();
lifecycle.lifecycleStateStream.listen((event) async {
if (defaultTargetPlatform == TargetPlatform.android) {
final state = lifecycle.toAndroid(event);
switch (state) {
case AndroidLifecycleState.paused:
v2rayService.disconnect();
            // 這裏暫存目前網址,以免到時重新連線又回到首頁 
url = (await webViewController.getUrl()).toString();
break;
case AndroidLifecycleState.started:
v2rayService.connect();
break;
default:
break;
}
print('android狀態: $state');
}
// else if (defaultTargetPlatform == TargetPlatform.iOS) {
// final state = lifecycle.toIos(event);
// print('ios狀態: $state');
// }
});
}

@override
Widget build(BuildContext context) {
return PopScope(
canPop: false,
onPopInvoked: (bool didPop) async {
if (await webViewController?.canGoBack() ?? false) {
webViewController?.goBack();
} else {
SystemNavigator.pop();
}
},
child: Scaffold(
body: !v2rayService.isConnected
? Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
ElevatedButton(
child: new Text("Connect"),
onPressed: () => v2rayService.connect(),
),
Container(height: 20.0), //SizedBox(height: 20.0),
],
)
: InAppWebView(
initialUrlRequest: URLRequest(url: WebUri(url)),
onWebViewCreated: (controller) {
webViewController = controller;
},
onReceivedError: (controller, req, error) {
                      // 這裡只給一次重整機會,防止遞迴
if (!v2rayService.hasReload.value) {
controller.reload();
v2rayService.hasReload.value = true;
print("req: ${req}");
print("error: ${error}");
}
},
onConsoleMessage: (controller, consoleMessage) {
print('Console message: ${consoleMessage.message}');
},
initialSettings: InAppWebViewSettings(
mixedContentMode:
MixedContentMode.MIXED_CONTENT_ALWAYS_ALLOW),
)));
}
}

最後也是最重要的步驟,就是要耐心的等,等到老闆沒耐心的時候,再把apk交出去,然後領績效獎金!!(咦!!

2023年4月20日 星期四

git刪除(遠端)分支

因為VSCode沒有提供這個指令,而且也不常用到,所以我老是忘記,在此組個紀錄

git push [REMOTE_NAME] :[BRANCH_NAME]

想不到是用push吧,記得在要刪除的分支前面加個冒號!

順便講一下刪除本地分支的語法,因為VSCode有,所以其實也不用特別記,只是想講

git branch -d [BRANCH_NAME]


2016年10月20日 星期四

連猴子都會用 Git

連猴子都會用 Git


前言:
進行Git學習與操作,請讀者發揮一點想像力,如果可以,盡量讓思緒保持請清晰。因為所謂版控,就是把 - 原本很單純的單線程檔案管理,變成多線程(branch),多節點(commit),多開發者(dev),以達到半自動化的管理,與程式的一致性,讓開發者不會發生改了又改,改了又改,改了又改的狀況(小編之前真的遇過)。當然隨著需求與操作習慣的不同,複雜度也會隨之不同,也可以極有條理,與優雅的版控程式,之所以這麼說,只是想先幫各為打個預防針,如果你準備好了,那就開始吧。
安裝Git
當然,沒有主程式我們玩個毛阿,在Window環境下,主程式 Git 下載安裝即可
俗話說得好:工欲善其事,必先利其器。在這麼先進的時代,你還會想要去記指令嗎?難道你還在用筆記本來開發程式嗎!不!你不會!所以在此小編要推薦大家一套網路上很常用也很好用的工具 Tortoisegit ,大家可以選用適合自己的作業環境的版本。(Tortoisegit只有windows版本,有錢如Kevin Jobs,使用mac的同事,有另外一套叫做SourceTree,只是這就不在今天討論,但觀念及操作方法是差不多的)
安裝好Tortoisegit只要在任意資料夾上(explorer任意處)按右鍵,就可以看到一隻小烏龜,及代表安裝完成。

Git環境設定

設定git資料夾(請參閱自己所安裝目錄)


Clone第一個專案吧
溫馨提示:即使Clone專案,也不代表你就是專案開發的一員,就算你更改了主線程式碼也沒辦法commit,如果你只是要參考該專案,可以用"Download Zip"(如果有的話),或是clone完後將.git的隱藏資料夾刪除,也可以使用Fork到自己的專案下。
因為公司專案大部分都是團隊開發,所以小編這邊教clone起步會稍微省事些,理所當然專案必須先在線上先開好(包含開發人員的管控)。若只是想在本機端進行版控(不上線),可以在要版控的資料夾內按右鍵,點選Git Create repository here,開始進行版控(但這不在今天教學內容中)
登入Git的管理平台後取得專案的Clone Url(預設已經開好專案)

在放置專案的資料夾中按右鍵,選Git Clone...

將複製的網址貼上,並確認資料夾是否正確,最後按下OK(本機資料夾必須是空白資料夾

Clone前會再次確認密碼(請輸入相對應帳號的密碼)
乖乖讓他跑完,專案就出現了,剩下的就可以在單機執行處理了

下載中

下載完畢


在正式開始前來個觀念解說吧
每個專案,最一開始都只有一條主線(master),每個開發者都會擁有自己的倉庫(Repo),所有的行為都是在本機上進行,包刮拉分支,與TFS不同的地方,我們只會看到一個資料夾!
大家可以想像一下自己身處在一個倉庫之中(資料夾),倉庫中的檔案可以來自不同的虛擬倉庫(Branch),而自己可以決定在現在實體倉庫中的檔案是來自(switch/checkout)哪個虛擬倉庫。每個異動的檔案(file)再分倉(commit)之前,都是實際存放於實體倉庫之中,無論我現在切換到哪個虛擬倉庫,檔案都會存在,直到分倉完畢,檔案才會只屬於該倉所有,只會在選擇該虛擬倉庫顯示。上述的所有行為直到push(check in)出去才會同步到線上(共用的倉庫)。概念圖如下圖
特殊情況: 再A分支上建立一個A資料夾,並新增一個A檔案(commit>push)(只單純新增資料夾是不給commit的),再新增一個B檔案,此時切到B分支會如何呢? 答案是: 會出現一個A資料夾,而裡面只有B檔案


接下來解釋些常用行為
Fetch(取):遠端數據資訊更新,緩存在本機之中,不區別分支,全部都會更新。
ex: A更新了master分支的內容,B若未執行Fetch,remotes/origin/master所看到的資訊都會是舊的

pull(拉): 將線上數據抓下來(單一分支),也可以說是Fetch+Merge


commit(確認送出): 將修改確認送進指定分支的倉庫中(確認送出的分支,也可在此創立新分支)
 (Message一定要打清楚,以做未來回朔參考) (Change選擇要送出的部分)


push(推): 將修改推送至線上(可選擇是否將所有分支送出)(若只是想在本機上做版控,壓根可以不用管這個動作)


一般來說,最理想的狀況就是,打完 > push > 無衝突 > 收工 > 放飯!只是一但團隊進行開發,事情就往往不盡如人意,偶爾會發生跟同事改到同一個檔案,好死不死你又剛好慢他一秒push,這時,流程就要稍微改一下了,變成 打完 > push > 有衝突 > SHIT! > pull > 解除衝突(手動Merge) > commit > push > 收工 > 放飯!當然如果修改內容不要太複雜不會花太多時間的。原則上你會看到如下畫面。

push產生衝突
(可直間點選左下角pull進行檔案合併)


pull衝突發生
(是否立馬解決,一般點是)


衝突檔案
(衝突檔案上點兩下開始編輯)


編輯畫面
(簡單說就是針對紅色的行數進行要你死還是我亡的選擇,也可以選擇都留)

(四個選項分別是:
使用這行(這時看你是在Local or Remote留下的行會不一樣)
以這檔案為主
兩行都留main在there前面
兩行都留main在there後面)


編輯完按綠色勾勾合併

編輯完,記得要再commit一次,之後再進行push

Revert(還原): 與網路上所說回朔之前的commit不同,在Tortoisegit當中的revert同TFS的Undo,可還原檔案,唯一條件是再commit前

Create branch:新增分支(可選擇來源的線,新增完不須commit,直接push即可上線)

Switch/Checkout:
切換分支
刪除分支
回朔之前的version(commit)
(此即網路上所說revert功能)
(若只是想看當時狀態,回朔時可不建立新的分支,只要再切換分支就會還原)
(回朔後所建立新的分支,只能再往前回朔,不能往後回朔)

Merge(合併)
選擇合併來源
合併完成
合併完成即已commit狀態

Create Tag
這功能就類似書籤,在眾多commit之中塞一個書籤,因為message內容偏向於這次修改的內容,較不適合作為書籤使用,可在完成每件事之後新增一個Tag,類似史記編年史一般,方便未來回朔記得名稱不能有空白

Show Log
看所有commit紀錄,資料異動

Delete and add to ingore list(取消檔案(資料夾),不再受版控控制)
這裡要來小小澄清一下,這裡有三種情況
情況一(只有你電腦中有這檔案,還沒push): 那這情形很單純,這功能會把該檔案的檔名跟路徑加到根目錄中的,gitignore中,讓檔案被git忽略版控
情況二(你不小心已經push出去了,線上有這檔案,但所幸還沒被別人pull): 這情況除了會做情況一的事,push出去,同時還會刪除線上檔案,因為線上如果檔案還存在,git是不會忽略該檔案的版控的(手動忽略時該注意)
情況三(你push出去,別人也不幸的pull了下來): 除了會做前兩個情況的事,做完後,如果別人pull檔案,因為會收到刪除的指令,所以該應當忽略的檔案會被刪除,所以如果是自動生成的檔案可以讓她刪除,如果是config之類,各自主機要保持不一樣的內容,卻又不能刪除的檔案,請做好協調並告知對方手動備份,
ps. 可以擬一份文件,說那些檔案被忽略,初次clone是抓不到的,必須手動新增,方便作業。針對.gitignore的用法,自己上網google好了很簡單啦~

Add: 讓檔案重回版控懷抱,或者有新加入的檔案(資料夾)也可以不藉由commit,而是透過add來加入版控


小訣竅:
更改右鍵所會出現的快捷鍵:
setting中

操作不如預期時的解決方式:
原則上在進行任何動作時,只要看到藍色就是成功,看到紅色就是失敗,而失敗時左下角通常都會有協助你完成動作的指示,類似下圖,在push時失敗,因為有衝突,所以要pull更新檔案方能繼續。

免重複輸入密碼的大絕招:
打開設定 > Git > Remote > origin > URL 稍微修改一下
原始 : http://(帳號)@git.newegg.org/scm/tcbb/kafka.git 
修改後 : http://(帳號):(你的密碼)@git.newegg.org/scm/tcbb/kafka.git在帳號後面先增一個冒號跟密碼,假設帳號是qwer,密碼是123,修改後就會變成 : http://qwer:123@git.newegg.org/scm/tcbb/kafka.git。
這方法簡單又快速,缺點就是密碼沒有加密,安全性欠佳。
所以結論就是要嘛圖個方便,要嘛就設跟其他網站不一樣的帳號或密碼,被盜了也沒差,要嘛就嚴禁老王接近你的電腦吧。

特殊狀況:
網路上有些Git資源,帳號要用信箱的,因為含有"@",所以clone的網址會辨識出錯,變成如下
http://git.newegg.org/scm/tcbb/kafka.git 
然後每次對線上動作都要重新輸入帳號跟密碼,這時.......只能乖乖認命了,天下何其大,為什麼要在這裡屈就呢,換一套吧! 

小結:一路說到這裡,大家差不多已經能學會大部分Git會用到的操作,可以順利完成版本控制,準時下班了。But!! 人生最厲害的就是這個But!! 明天上班會發生什麼事呢? 這又是另外一個故事了........