Skip to content

Giải pháp Workaround: Vite Plugin

Mục tiêu bài học

  • Hiểu 3-Layer Defense Strategy
  • Nắm implementation từng layer
  • Biết cách verify solution
  • Học troubleshooting techniques
  • Tìm hiểu alternative solutions

Strategy: 3-Layer Defense

Giải pháp bao gồm 3 lớp bảo vệ để đảm bảo MediaPipe hoạt động trong production:

Layer 1: Flexible Import (Wrapper Class)
    ↓ (if fails)
Layer 2: Vite Plugin (Patch Exports)
    ↓ (if fails)
Layer 3: Build Config (Preserve Structure)

Tại sao cần 3 lớp?

  • Lớp 1: Xử lý các biến thể import tại thời gian chạy
  • Lớp 2: Sửa các vấn đề export tại thời gian build
  • Lớp 3: Ngăn build làm hỏng cấu trúc

Layer 1: Flexible Import

Implementation trong Wrapper Class

Trong MediaPipeFaceMeshWrapper.ts, sử dụng dynamic import với nhiều fallback:

typescript
// MediaPipeFaceMeshWrapper.ts
let FaceMeshClass: any = null;

async initialize(): Promise<void> {
  // Dynamic import FaceMesh to avoid build issues
  if (!FaceMeshClass) {
    try {
      const faceMeshModule = await import('@mediapipe/face_mesh');

      // Try multiple ways to get FaceMesh constructor

      // 1. Named export
      if (faceMeshModule.FaceMesh && typeof faceMeshModule.FaceMesh === 'function') {
        FaceMeshClass = faceMeshModule.FaceMesh;
      }
      // 2. Default.FaceMesh
      else if (faceMeshModule.default?.FaceMesh && typeof faceMeshModule.default.FaceMesh === 'function') {
        FaceMeshClass = faceMeshModule.default.FaceMesh;
      }
      // 3. Default as constructor
      else if (faceMeshModule.default && typeof faceMeshModule.default === 'function') {
        FaceMeshClass = faceMeshModule.default;
      }
      // 4. Any access (bypass type checking)
      else if ((faceMeshModule as any).FaceMesh) {
        const potentialClass = (faceMeshModule as any).FaceMesh;
        if (typeof potentialClass === 'function') {
          FaceMeshClass = potentialClass;
        }
      }
      // 5. Iterate all properties
      if (!FaceMeshClass) {
        for (const key in faceMeshModule) {
          const value = (faceMeshModule as any)[key];
          if (value && typeof value === 'function' && key.toLowerCase().includes('face')) {
            console.log(`Found potential FaceMesh at key: ${key}`);
            FaceMeshClass = value;
            break;
          }
        }
      }

      if (!FaceMeshClass || typeof FaceMeshClass !== 'function') {
        throw new Error('FaceMesh constructor not found in module');
      }
    } catch (error) {
      console.error('[MediaPipeFaceMeshWrapper] Failed to import FaceMesh:', error);
      throw error;
    }
  }

  // Create instance
  this.faceMesh = new FaceMeshClass({
    locateFile: (file: string) => {
      return `https://cdn.jsdelivr.net/npm/@mediapipe/face_mesh/${file}`;
    },
  });
}

Tại sao import động?

  • ✅ Tránh các vấn đề phân tích tại thời gian build
  • ✅ Xử lý các cấu trúc module khác nhau
  • ✅ Hoạt động trong cả dev và production
  • ✅ Cung cấp nhiều chiến lược dự phòng

Layer 2: Vite Plugin

Plugin Implementation

Tạo custom Vite plugin để patch MediaPipe exports:

typescript
// mediaPipePlugin.ts
import fs from "fs";
import path from "path";
import type { Plugin } from "vite";

function mediaPipePlugin(): Plugin {
  return {
    name: "mediapipe_workaround",
    load(id: string) {
      // Check if this is the MediaPipe face_mesh.js file
      if (
        path.basename(id) === "face_mesh.js" &&
        id.includes("@mediapipe/face_mesh")
      ) {
        try {
          let code = fs.readFileSync(id, "utf-8");

          // Add explicit export for FaceMesh if it's not already there
          // This ensures FaceMesh is available as a named export
          if (!code.includes("exports.FaceMesh")) {
            code += "\nexports.FaceMesh = FaceMesh;";
          }

          console.log("[mediaPipePlugin] Patched face_mesh.js exports");
          return { code };
        } catch (error) {
          console.error(
            "[mediaPipePlugin] Error patching face_mesh.js:",
            error
          );
          return null;
        }
      }
      return null;
    },
  };
}

export default mediaPipePlugin;

Plugin Hooks

Plugin sử dụng hook load để:

  • Chặn việc tải file
  • Kiểm tra xem có phải MediaPipe face_mesh.js không
  • Sửa export trước khi Rollup xử lý

How It Works

1. Rollup tries to load @mediapipe/face_mesh/face_mesh.js

2. Plugin intercepts via load() hook

3. Read file content

4. Check if exports.FaceMesh exists

5. If not, add: exports.FaceMesh = FaceMesh;

6. Return patched code

Layer 3: Build Config

Vite Config Settings

Cấu hình trong vite.config.ts:

typescript
// vite.config.ts
import mediaPipePlugin from "./mediaPipePlugin";

export default {
  build: {
    commonjsOptions: {
      transformMixedEsModules: true,
      include: [/node_modules/, /tailwind.config.js/],
      exclude: ["mediaPipePlugin.ts"],
      extensions: [".js", ".cjs", ".jsx"],
    },
    rollupOptions: {
      // Apply plugin in Rollup phase
      plugins: [mediaPipePlugin()],
      output: {
        manualChunks: (id) => {
          // Separate MediaPipe into its own chunk
          if (id.includes("@mediapipe/face_mesh")) {
            return "mediapipe-face-mesh";
          }
        },
        // Preserve exports for MediaPipe
        exports: "named",
      },
    },
    // Increase chunk size warning limit for MediaPipe (~7MB)
    chunkSizeWarningLimit: 1000,
  },
  optimizeDeps: {
    // Include MediaPipe in pre-bundling
    include: ["@mediapipe/face_mesh"],
    esbuildOptions: {
      // Preserve class names for MediaPipe
      keepNames: true,
    },
  },
};

Key Config Settings

1. manualChunks

typescript
manualChunks: (id) => {
  if (id.includes("@mediapipe/face_mesh")) {
    return "mediapipe-face-mesh";
  }
};

Tại sao?

  • Tách MediaPipe thành chunk riêng
  • Ngăn các vấn đề chia nhỏ code
  • Đảm bảo thứ tự tải đúng

2. exports: 'named'

typescript
output: {
  exports: 'named',
}

Tại sao?

  • Giữ nguyên named exports (FaceMesh)
  • Ngăn Rollup chuyển đổi exports không đúng

3. optimizeDeps.include

typescript
optimizeDeps: {
  include: ['@mediapipe/face_mesh'],
}

Tại sao?

  • Pre-bundle MediaPipe trong chế độ dev
  • Đảm bảo hành vi nhất quán giữa dev và prod

4. esbuildOptions.keepNames

typescript
esbuildOptions: {
  keepNames: true,
}

Tại sao?

  • Giữ nguyên tên class trong quá trình minify
  • Giữ tên FaceMesh sau khi build

Implementation Steps

Step 1: Create Plugin File

bash
# Create plugin file
touch mediaPipePlugin.ts

Step 2: Implement Plugin

typescript
// Copy plugin code from above
// mediaPipePlugin.ts

Step 3: Update Vite Config

typescript
// vite.config.ts
import mediaPipePlugin from "./mediaPipePlugin";

export default {
  build: {
    rollupOptions: {
      plugins: [mediaPipePlugin()],
      // ... other config
    },
  },
};

Step 4: Update Wrapper Class

typescript
// MediaPipeFaceMeshWrapper.ts
// Use dynamic import with fallbacks (from Layer 1)

Verification Steps

1. Development Mode

bash
npm run dev

Kỳ vọng:

  • ✅ Không có lỗi console
  • ✅ FaceMesh tải thành công
  • ✅ Phát hiện khuôn mặt hoạt động

Check:

typescript
// In browser console
console.log("FaceMesh loaded:", typeof FaceMesh === "function");

2. Production Build

bash
npm run build
npm run preview

Kỳ vọng:

  • ✅ Build thành công
  • ✅ Không có lỗi "FaceMesh is not a constructor"
  • ✅ Phát hiện khuôn mặt hoạt động

Check:

  • Open browser console
  • Check for MediaPipe errors
  • Test face detection

3. Check Bundle

bash
npm run build
ls -la dist/assets/ | grep mediapipe

Kỳ vọng:

  • ✅ Chunk MediaPipe tồn tại: mediapipe-face-mesh-*.js
  • ✅ Kích thước chunk ~7MB
  • ✅ Không có nhiều chunk cho MediaPipe

4. Check Network Tab

Kỳ vọng:

  • mediapipe-face-mesh-*.js tải thành công
  • ✅ File WASM tải từ CDN
  • ✅ Không có lỗi 404

Troubleshooting

Issue 1: Plugin không chạy

Triệu chứng: Console không thấy log [mediaPipePlugin] Patched...

Giải pháp:

typescript
// Kiểm tra plugin được áp dụng ở giai đoạn đúng
// Phải ở trong build.rollupOptions.plugins
plugins: [mediaPipePlugin()], // ✅ Đúng
// KHÔNG ở trong mảng plugins cấp cao nhất ❌

Issue 2: FaceMesh vẫn undefined

Triệu chứng: FaceMesh is not a constructor vẫn xuất hiện

Debug:

typescript
// Thêm nhiều logging hơn trong wrapper
console.log("[Wrapper] Các khóa module:", Object.keys(faceMeshModule));
console.log("[Wrapper] Cấu trúc module:", faceMeshModule);
console.log("[Wrapper] FaceMeshClass:", FaceMeshClass);

Giải pháp: Thêm nhiều chiến lược dự phòng hơn ở Lớp 1

Issue 3: Chunk không tách riêng

Triệu chứng: MediaPipe vẫn bundle vào main chunk

Kiểm tra:

typescript
// Xác minh hàm manualChunks
manualChunks: (id) => {
  console.log("[manualChunks] id:", id); // Log debug
  if (id.includes("@mediapipe/face_mesh")) {
    return "mediapipe-face-mesh";
  }
};

Giải pháp: Đảm bảo id.includes('@mediapipe/face_mesh') khớp đúng

Issue 4: Plugin patch không work

Triệu chứng: exports.FaceMesh vẫn không có sau build

Debug:

typescript
// Thêm nhiều logging hơn trong plugin
load(id: string) {
  if (path.basename(id) === 'face_mesh.js') {
    console.log('[Plugin] Đang xử lý file:', id);
    console.log('[Plugin] Code trước khi sửa:', code.substring(0, 100));
    // ... sửa code ...
    console.log('[Plugin] Code sau khi sửa:', code.substring(code.length - 100));
  }
}

Giải pháp: Kiểm tra xem đường dẫn file có khớp đúng không

Issue 5: Dynamic import fail

Triệu chứng: import('@mediapipe/face_mesh') ném lỗi

Debug:

typescript
try {
  const module = await import("@mediapipe/face_mesh");
  console.log("[Import] Thành công, các khóa module:", Object.keys(module));
} catch (error) {
  console.error("[Import] Thất bại:", error);
}

Giải pháp:

  • Kiểm tra tab network cho lỗi 404
  • Xác minh MediaPipe có trong dependencies
  • Kiểm tra CDN có thể truy cập được

Alternative Solutions

Giải pháp thay thế 1: CDN bên ngoài

Tải MediaPipe từ CDN thay vì bundle:

typescript
// vite.config.ts
build: {
  rollupOptions: {
    external: ['@mediapipe/face_mesh'],
    output: {
      globals: {
        '@mediapipe/face_mesh': 'MediaPipeFaceMesh',
      },
    },
  },
},
html
<!-- index.html -->
<script src="https://cdn.jsdelivr.net/npm/@mediapipe/face_mesh/face_mesh.js"></script>

Ưu điểm:

  • ✅ Tránh các vấn đề build
  • ✅ Kích thước bundle nhỏ hơn

Nhược điểm:

  • ❌ Yêu cầu kết nối internet
  • ❌ Phụ thuộc vào tính khả dụng của CDN

Giải pháp thay thế 2: Sao chép vào Public

Sao chép file MediaPipe vào thư mục public:

bash
# Copy MediaPipe to public
cp -r node_modules/@mediapipe/face_mesh public/mediapipe/
typescript
// Load from public
const faceMeshModule = await import("/mediapipe/face_mesh.js");

Ưu điểm:

  • ✅ Kiểm soát hoàn toàn file
  • ✅ Không có chuyển đổi build

Nhược điểm:

  • ❌ Bước sao chép thủ công
  • ❌ Thư mục public lớn hơn

Giải pháp thay thế 3: Webpack thay vì Vite

Chuyển sang Webpack xử lý MediaPipe tốt hơn:

bash
npm install --save-dev webpack webpack-cli

Ưu điểm:

  • ✅ Hỗ trợ WASM tốt hơn
  • ✅ Hệ sinh thái trưởng thành hơn

Nhược điểm:

  • ❌ Yêu cầu di chuyển hoàn toàn
  • ❌ Thời gian build chậm hơn

Giải pháp thay thế 4: Dùng phát hiện khuôn mặt khác

Chuyển sang thư viện thay thế:

  • TensorFlow.js
  • Face-api.js
  • MediaPipe qua package khác

Ưu điểm:

  • ✅ Có thể có hỗ trợ build tốt hơn
  • ✅ Các tính năng khác

Nhược điểm:

  • ❌ Yêu cầu viết lại hoàn toàn
  • ❌ Có thể không có tất cả tính năng

Best Practices

1. Luôn kiểm thử build production

bash
# Đừng chỉ dựa vào chế độ dev
npm run build
npm run preview
# Kiểm thử kỹ trước khi triển khai

2. Giám sát kích thước bundle

bash
# Kiểm tra kích thước chunk MediaPipe
npm run build -- --report

Kỳ vọng: ~7MB cho MediaPipe

3. Ghi log hoạt động plugin

typescript
// Thêm logging để theo dõi thực thi plugin
console.log("[mediaPipePlugin] Đang xử lý:", id);

4. Khóa phiên bản MediaPipe

json
// package.json
{
  "dependencies": {
    "@mediapipe/face_mesh": "0.4.1633559619" // Khóa phiên bản
  }
}

Tại sao? Cập nhật có thể làm hỏng workaround

5. Tài liệu hóa workaround

typescript
/**
 * WORKAROUND: Vite production build chuyển đổi MediaPipe không đúng
 * Plugin này sửa export để đảm bảo FaceMesh có sẵn
 * Xem: docs/ekyc/12-vite-plugin-fix.md
 */

Performance Impact

Kích thước Bundle

  • Không có sửa chữa: ~7MB (MediaPipe bundle không đúng → hỏng)
  • Có sửa chữa: ~7MB (MediaPipe bundle đúng → hoạt động)
  • Tác động kích thước: Tối thiểu (cùng kích thước, cấu trúc khác)

Hiệu suất Runtime

  • Không có tác động: Giải pháp chỉ sửa các vấn đề tại thời gian build
  • Thời gian tải: Nhanh hơn một chút (chunk riêng có thể tải song song)

Thời gian Build

  • Chi phí plugin: ~100ms mỗi build
  • Chấp nhận được: Đánh đổi cho build production hoạt động

Tổng kết

Chiến lược 3-Layer Defense:

  1. Lớp 1 (Flexible Import): Xử lý các biến thể runtime
  2. Lớp 2 (Vite Plugin): Sửa export tại thời gian build
  3. Lớp 3 (Build Config): Giữ nguyên cấu trúc module

Nguyên tắc chính:

  • ✅ Nhiều dự phòng ở mỗi lớp
  • ✅ Kiểm thử build production, không chỉ dev
  • ✅ Giám sát kích thước và cấu trúc bundle
  • ✅ Tài liệu hóa workaround đầy đủ

Kết quả: MediaPipe hoạt động trong cả build dev và production!

Bài học tiếp theo

END of eKYC Documentation 🎉

Nếu cần thêm thông tin, xem:

Internal documentation for iNET Portal