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:
// 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:
// 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 codeLayer 3: Build Config
Vite Config Settings
Cấu hình trong vite.config.ts:
// 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
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'
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
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
esbuildOptions: {
keepNames: true,
}Tại sao?
- Giữ nguyên tên class trong quá trình minify
- Giữ tên
FaceMeshsau khi build
Implementation Steps
Step 1: Create Plugin File
# Create plugin file
touch mediaPipePlugin.tsStep 2: Implement Plugin
// Copy plugin code from above
// mediaPipePlugin.tsStep 3: Update Vite Config
// vite.config.ts
import mediaPipePlugin from "./mediaPipePlugin";
export default {
build: {
rollupOptions: {
plugins: [mediaPipePlugin()],
// ... other config
},
},
};Step 4: Update Wrapper Class
// MediaPipeFaceMeshWrapper.ts
// Use dynamic import with fallbacks (from Layer 1)Verification Steps
1. Development Mode
npm run devKỳ 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:
// In browser console
console.log("FaceMesh loaded:", typeof FaceMesh === "function");2. Production Build
npm run build
npm run previewKỳ 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
npm run build
ls -la dist/assets/ | grep mediapipeKỳ 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-*.jstả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:
// 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:
// 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:
// 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:
// 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:
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:
// vite.config.ts
build: {
rollupOptions: {
external: ['@mediapipe/face_mesh'],
output: {
globals: {
'@mediapipe/face_mesh': 'MediaPipeFaceMesh',
},
},
},
},<!-- 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:
# Copy MediaPipe to public
cp -r node_modules/@mediapipe/face_mesh public/mediapipe/// 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:
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
# Đừng chỉ dựa vào chế độ dev
npm run build
npm run preview
# Kiểm thử kỹ trước khi triển khai2. Giám sát kích thước bundle
# Kiểm tra kích thước chunk MediaPipe
npm run build -- --reportKỳ vọng: ~7MB cho MediaPipe
3. Ghi log hoạt động plugin
// 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
// 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
/**
* 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:
- ✅ Lớp 1 (Flexible Import): Xử lý các biến thể runtime
- ✅ Lớp 2 (Vite Plugin): Sửa export tại thời gian build
- ✅ 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: