Browse Source
Remove obsolete Vite configuration files that are no longer used by the build system and update BUILDING.md to accurately reflect the current configuration structure. **Removed Files:** - vite.config.ts (47 lines) - Legacy configuration file - vite.config.mts (70 lines) - Unused "main" configuration file **Updated Documentation:** - BUILDING.md - Corrected Vite configuration section to show actual usage **Current Configuration Structure:** - vite.config.web.mts - Used by build-web.sh - vite.config.electron.mts - Used by build-electron.sh - vite.config.capacitor.mts - Used by npm run build:capacitor - vite.config.common.mts - Shared configuration utilities - vite.config.utils.mts - Configuration utility functions **Benefits:** - Eliminates confusion about which config files to use - Removes 117 lines of unused configuration code - Documentation now matches actual build system usage - Cleaner, more maintainable configuration structure **Impact:** - No functional changes to build process - All platform builds continue to work correctly - Reduced configuration complexity and maintenance overheadpull/142/head
8 changed files with 5 additions and 2724 deletions
@ -1,662 +0,0 @@ |
|||||
# Vue 3 + Vite + vue-facing-decorator: Lazy Loading Patterns & Best Practices |
|
||||
|
|
||||
## Overview |
|
||||
|
|
||||
This document provides comprehensive guidance on implementing lazy loading and code splitting in Vue 3 applications using Vite and `vue-facing-decorator`. The patterns demonstrated here optimize bundle size, improve initial load times, and enhance user experience through progressive loading. |
|
||||
|
|
||||
**Author:** Matthew Raymer |
|
||||
**Version:** 1.0.0 |
|
||||
**Last Updated:** 2024 |
|
||||
|
|
||||
## Table of Contents |
|
||||
|
|
||||
1. [Why Lazy Loading Matters](#why-lazy-loading-matters) |
|
||||
2. [Vite Configuration for Optimal Code Splitting](#vite-configuration-for-optimal-code-splitting) |
|
||||
3. [Lazy Loading Patterns](#lazy-loading-patterns) |
|
||||
4. [Component-Level Lazy Loading](#component-level-lazy-loading) |
|
||||
5. [Route-Level Lazy Loading](#route-level-lazy-loading) |
|
||||
6. [Library-Level Lazy Loading](#library-level-lazy-loading) |
|
||||
7. [Performance Monitoring](#performance-monitoring) |
|
||||
8. [Best Practices](#best-practices) |
|
||||
9. [Common Pitfalls](#common-pitfalls) |
|
||||
10. [Examples](#examples) |
|
||||
|
|
||||
## Why Lazy Loading Matters |
|
||||
|
|
||||
### Performance Benefits |
|
||||
|
|
||||
- **Faster Initial Load**: Only load code needed for the current view |
|
||||
- **Reduced Bundle Size**: Split large applications into smaller chunks |
|
||||
- **Better Caching**: Independent chunks can be cached separately |
|
||||
- **Improved User Experience**: Progressive loading with loading states |
|
||||
|
|
||||
### When to Use Lazy Loading |
|
||||
|
|
||||
✅ **Good Candidates for Lazy Loading:** |
|
||||
- Heavy components (data processing, 3D rendering) |
|
||||
- Feature-specific components (QR scanner, file uploader) |
|
||||
- Route-based components |
|
||||
- Large third-party libraries (ThreeJS, Chart.js) |
|
||||
- Components with conditional rendering |
|
||||
|
|
||||
❌ **Avoid Lazy Loading:** |
|
||||
- Core UI components used everywhere |
|
||||
- Small utility components |
|
||||
- Components needed for initial render |
|
||||
- Components with frequent usage patterns |
|
||||
|
|
||||
## Vite Configuration for Optimal Code Splitting |
|
||||
|
|
||||
### Enhanced Build Configuration |
|
||||
|
|
||||
```typescript |
|
||||
// vite.config.optimized.mts |
|
||||
export async function createOptimizedBuildConfig(mode: string): Promise<UserConfig> { |
|
||||
return { |
|
||||
build: { |
|
||||
rollupOptions: { |
|
||||
output: { |
|
||||
// Enhanced manual chunks for better code splitting |
|
||||
manualChunks: { |
|
||||
// Vendor chunks for better caching |
|
||||
'vue-vendor': ['vue', 'vue-router', 'pinia'], |
|
||||
'ui-vendor': ['@fortawesome/fontawesome-svg-core', '@fortawesome/vue-fontawesome'], |
|
||||
'crypto-vendor': ['@ethersproject/wallet', '@ethersproject/hdnode'], |
|
||||
'sql-vendor': ['@jlongster/sql.js', 'absurd-sql', 'dexie'], |
|
||||
'qr-vendor': ['qrcode', 'jsqr', 'vue-qrcode-reader'], |
|
||||
'three-vendor': ['three', '@tweenjs/tween.js'], |
|
||||
'utils-vendor': ['luxon', 'ramda', 'zod', 'axios'], |
|
||||
// Platform-specific chunks |
|
||||
...(isCapacitor && { |
|
||||
'capacitor-vendor': ['@capacitor/core', '@capacitor/app'] |
|
||||
}) |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
}; |
|
||||
} |
|
||||
``` |
|
||||
|
|
||||
### Key Configuration Features |
|
||||
|
|
||||
1. **Manual Chunks**: Group related dependencies for better caching |
|
||||
2. **Platform-Specific Chunks**: Separate native and web dependencies |
|
||||
3. **Vendor Separation**: Keep third-party libraries separate from app code |
|
||||
4. **Dynamic Imports**: Enable automatic code splitting for dynamic imports |
|
||||
|
|
||||
## Lazy Loading Patterns |
|
||||
|
|
||||
### 1. Component-Level Lazy Loading |
|
||||
|
|
||||
#### Basic Pattern with defineAsyncComponent |
|
||||
|
|
||||
```typescript |
|
||||
import { Component, Vue } from 'vue-facing-decorator'; |
|
||||
import { defineAsyncComponent } from 'vue'; |
|
||||
|
|
||||
@Component({ |
|
||||
name: 'LazyLoadingExample', |
|
||||
components: { |
|
||||
LazyHeavyComponent: defineAsyncComponent({ |
|
||||
loader: () => import('./sub-components/HeavyComponent.vue'), |
|
||||
loadingComponent: { |
|
||||
template: '<div class="loading">Loading...</div>' |
|
||||
}, |
|
||||
errorComponent: { |
|
||||
template: '<div class="error">Failed to load</div>' |
|
||||
}, |
|
||||
delay: 200, |
|
||||
timeout: 10000 |
|
||||
}) |
|
||||
} |
|
||||
}) |
|
||||
export default class LazyLoadingExample extends Vue { |
|
||||
// Component logic |
|
||||
} |
|
||||
``` |
|
||||
|
|
||||
#### Advanced Pattern with Suspense |
|
||||
|
|
||||
```vue |
|
||||
<template> |
|
||||
<Suspense> |
|
||||
<template #default> |
|
||||
<LazyHeavyComponent v-if="showComponent" /> |
|
||||
</template> |
|
||||
<template #fallback> |
|
||||
<div class="loading-fallback"> |
|
||||
<div class="spinner"></div> |
|
||||
<p>Loading component...</p> |
|
||||
</div> |
|
||||
</template> |
|
||||
</Suspense> |
|
||||
</template> |
|
||||
``` |
|
||||
|
|
||||
### 2. Conditional Loading |
|
||||
|
|
||||
```typescript |
|
||||
@Component({ |
|
||||
name: 'ConditionalLazyLoading' |
|
||||
}) |
|
||||
export default class ConditionalLazyLoading extends Vue { |
|
||||
showHeavyComponent = false; |
|
||||
showQRScanner = false; |
|
||||
|
|
||||
// Lazy load based on user interaction |
|
||||
async toggleHeavyComponent(): Promise<void> { |
|
||||
this.showHeavyComponent = !this.showHeavyComponent; |
|
||||
|
|
||||
if (this.showHeavyComponent) { |
|
||||
// Preload component for better UX |
|
||||
await this.preloadComponent(() => import('./HeavyComponent.vue')); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
private async preloadComponent(loader: () => Promise<any>): Promise<void> { |
|
||||
try { |
|
||||
await loader(); |
|
||||
} catch (error) { |
|
||||
console.warn('Preload failed:', error); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
``` |
|
||||
|
|
||||
### 3. Library-Level Lazy Loading |
|
||||
|
|
||||
```typescript |
|
||||
@Component({ |
|
||||
name: 'LibraryLazyLoading' |
|
||||
}) |
|
||||
export default class LibraryLazyLoading extends Vue { |
|
||||
private threeJS: any = null; |
|
||||
|
|
||||
async loadThreeJS(): Promise<void> { |
|
||||
if (!this.threeJS) { |
|
||||
// Lazy load ThreeJS only when needed |
|
||||
this.threeJS = await import('three'); |
|
||||
console.log('ThreeJS loaded successfully'); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
async initialize3DScene(): Promise<void> { |
|
||||
await this.loadThreeJS(); |
|
||||
|
|
||||
// Use ThreeJS after loading |
|
||||
const scene = new this.threeJS.Scene(); |
|
||||
// ... rest of 3D setup |
|
||||
} |
|
||||
} |
|
||||
``` |
|
||||
|
|
||||
## Component-Level Lazy Loading |
|
||||
|
|
||||
### Heavy Data Processing Component |
|
||||
|
|
||||
```typescript |
|
||||
// HeavyComponent.vue |
|
||||
@Component({ |
|
||||
name: 'HeavyComponent' |
|
||||
}) |
|
||||
export default class HeavyComponent extends Vue { |
|
||||
@Prop({ required: true }) readonly data!: any; |
|
||||
|
|
||||
async processData(): Promise<void> { |
|
||||
// Process data in batches to avoid blocking UI |
|
||||
const batchSize = 10; |
|
||||
const items = this.data.items; |
|
||||
|
|
||||
for (let i = 0; i < items.length; i += batchSize) { |
|
||||
const batch = items.slice(i, i + batchSize); |
|
||||
await this.processBatch(batch); |
|
||||
|
|
||||
// Allow UI to update |
|
||||
await this.$nextTick(); |
|
||||
await new Promise(resolve => setTimeout(resolve, 10)); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
``` |
|
||||
|
|
||||
### Camera-Dependent Component |
|
||||
|
|
||||
```typescript |
|
||||
// QRScannerComponent.vue |
|
||||
@Component({ |
|
||||
name: 'QRScannerComponent' |
|
||||
}) |
|
||||
export default class QRScannerComponent extends Vue { |
|
||||
async mounted(): Promise<void> { |
|
||||
// Initialize camera only when component is mounted |
|
||||
await this.initializeCamera(); |
|
||||
} |
|
||||
|
|
||||
async initializeCamera(): Promise<void> { |
|
||||
try { |
|
||||
const devices = await navigator.mediaDevices.enumerateDevices(); |
|
||||
this.cameras = devices.filter(device => device.kind === 'videoinput'); |
|
||||
this.hasCamera = this.cameras.length > 0; |
|
||||
} catch (error) { |
|
||||
console.error('Camera initialization failed:', error); |
|
||||
this.hasCamera = false; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
``` |
|
||||
|
|
||||
## Route-Level Lazy Loading |
|
||||
|
|
||||
### Vue Router Configuration |
|
||||
|
|
||||
```typescript |
|
||||
// router/index.ts |
|
||||
import { createRouter, createWebHistory } from 'vue-router'; |
|
||||
|
|
||||
const routes = [ |
|
||||
{ |
|
||||
path: '/', |
|
||||
name: 'Home', |
|
||||
component: () => import('@/views/HomeView.vue') |
|
||||
}, |
|
||||
{ |
|
||||
path: '/heavy-feature', |
|
||||
name: 'HeavyFeature', |
|
||||
component: () => import('@/views/HeavyFeatureView.vue'), |
|
||||
// Preload on hover for better UX |
|
||||
beforeEnter: (to, from, next) => { |
|
||||
if (from.name) { |
|
||||
// Preload component when navigating from another route |
|
||||
import('@/views/HeavyFeatureView.vue'); |
|
||||
} |
|
||||
next(); |
|
||||
} |
|
||||
} |
|
||||
]; |
|
||||
|
|
||||
export default createRouter({ |
|
||||
history: createWebHistory(), |
|
||||
routes |
|
||||
}); |
|
||||
``` |
|
||||
|
|
||||
### Route Guards with Lazy Loading |
|
||||
|
|
||||
```typescript |
|
||||
// router/guards.ts |
|
||||
export async function lazyLoadGuard(to: any, from: any, next: any): Promise<void> { |
|
||||
if (to.meta.requiresHeavyFeature) { |
|
||||
try { |
|
||||
// Preload heavy feature before navigation |
|
||||
await import('@/components/HeavyFeature.vue'); |
|
||||
next(); |
|
||||
} catch (error) { |
|
||||
console.error('Failed to load heavy feature:', error); |
|
||||
next('/error'); |
|
||||
} |
|
||||
} else { |
|
||||
next(); |
|
||||
} |
|
||||
} |
|
||||
``` |
|
||||
|
|
||||
## Library-Level Lazy Loading |
|
||||
|
|
||||
### Dynamic Library Loading |
|
||||
|
|
||||
```typescript |
|
||||
@Component({ |
|
||||
name: 'DynamicLibraryLoader' |
|
||||
}) |
|
||||
export default class DynamicLibraryLoader extends Vue { |
|
||||
private libraries: Map<string, any> = new Map(); |
|
||||
|
|
||||
async loadLibrary(name: string): Promise<any> { |
|
||||
if (this.libraries.has(name)) { |
|
||||
return this.libraries.get(name); |
|
||||
} |
|
||||
|
|
||||
let library: any; |
|
||||
|
|
||||
switch (name) { |
|
||||
case 'three': |
|
||||
library = await import('three'); |
|
||||
break; |
|
||||
case 'chart': |
|
||||
library = await import('chart.js'); |
|
||||
break; |
|
||||
case 'qr': |
|
||||
library = await import('jsqr'); |
|
||||
break; |
|
||||
default: |
|
||||
throw new Error(`Unknown library: ${name}`); |
|
||||
} |
|
||||
|
|
||||
this.libraries.set(name, library); |
|
||||
return library; |
|
||||
} |
|
||||
} |
|
||||
``` |
|
||||
|
|
||||
### Conditional Library Loading |
|
||||
|
|
||||
```typescript |
|
||||
@Component({ |
|
||||
name: 'ConditionalLibraryLoader' |
|
||||
}) |
|
||||
export default class ConditionalLibraryLoader extends Vue { |
|
||||
async loadPlatformSpecificLibrary(): Promise<void> { |
|
||||
if (process.env.VITE_PLATFORM === 'capacitor') { |
|
||||
// Load Capacitor-specific libraries |
|
||||
await import('@capacitor/camera'); |
|
||||
await import('@capacitor/filesystem'); |
|
||||
} else { |
|
||||
// Load web-specific libraries |
|
||||
await import('file-saver'); |
|
||||
await import('jszip'); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
``` |
|
||||
|
|
||||
## Performance Monitoring |
|
||||
|
|
||||
### Bundle Analysis |
|
||||
|
|
||||
```bash |
|
||||
# Analyze bundle size |
|
||||
npm run build |
|
||||
npx vite-bundle-analyzer dist |
|
||||
|
|
||||
# Monitor chunk sizes |
|
||||
npx vite-bundle-analyzer dist --mode=treemap |
|
||||
``` |
|
||||
|
|
||||
### Runtime Performance Monitoring |
|
||||
|
|
||||
```typescript |
|
||||
@Component({ |
|
||||
name: 'PerformanceMonitor' |
|
||||
}) |
|
||||
export default class PerformanceMonitor extends Vue { |
|
||||
private performanceMetrics = { |
|
||||
componentLoadTime: 0, |
|
||||
renderTime: 0, |
|
||||
memoryUsage: 0 |
|
||||
}; |
|
||||
|
|
||||
private measureComponentLoad(componentName: string): void { |
|
||||
const startTime = performance.now(); |
|
||||
|
|
||||
return () => { |
|
||||
const loadTime = performance.now() - startTime; |
|
||||
this.performanceMetrics.componentLoadTime = loadTime; |
|
||||
|
|
||||
console.log(`${componentName} loaded in ${loadTime.toFixed(2)}ms`); |
|
||||
|
|
||||
// Send to analytics |
|
||||
this.trackPerformance(componentName, loadTime); |
|
||||
}; |
|
||||
} |
|
||||
|
|
||||
private trackPerformance(component: string, loadTime: number): void { |
|
||||
// Send to analytics service |
|
||||
if (window.gtag) { |
|
||||
window.gtag('event', 'component_load', { |
|
||||
component_name: component, |
|
||||
load_time: loadTime |
|
||||
}); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
``` |
|
||||
|
|
||||
## Best Practices |
|
||||
|
|
||||
### 1. Loading States |
|
||||
|
|
||||
Always provide meaningful loading states: |
|
||||
|
|
||||
```vue |
|
||||
<template> |
|
||||
<Suspense> |
|
||||
<template #default> |
|
||||
<LazyComponent /> |
|
||||
</template> |
|
||||
<template #fallback> |
|
||||
<div class="loading-state"> |
|
||||
<div class="spinner"></div> |
|
||||
<p>Loading {{ componentName }}...</p> |
|
||||
<p class="loading-tip">{{ loadingTip }}</p> |
|
||||
</div> |
|
||||
</template> |
|
||||
</Suspense> |
|
||||
</template> |
|
||||
``` |
|
||||
|
|
||||
### 2. Error Handling |
|
||||
|
|
||||
Implement comprehensive error handling: |
|
||||
|
|
||||
```typescript |
|
||||
const LazyComponent = defineAsyncComponent({ |
|
||||
loader: () => import('./HeavyComponent.vue'), |
|
||||
errorComponent: { |
|
||||
template: ` |
|
||||
<div class="error-state"> |
|
||||
<h3>Failed to load component</h3> |
|
||||
<p>{{ error.message }}</p> |
|
||||
<button @click="retry">Retry</button> |
|
||||
</div> |
|
||||
`, |
|
||||
props: ['error'], |
|
||||
methods: { |
|
||||
retry() { |
|
||||
this.$emit('retry'); |
|
||||
} |
|
||||
} |
|
||||
}, |
|
||||
onError(error, retry, fail, attempts) { |
|
||||
if (attempts <= 3) { |
|
||||
console.warn(`Retrying component load (attempt ${attempts})`); |
|
||||
retry(); |
|
||||
} else { |
|
||||
console.error('Component failed to load after 3 attempts'); |
|
||||
fail(); |
|
||||
} |
|
||||
} |
|
||||
}); |
|
||||
``` |
|
||||
|
|
||||
### 3. Preloading Strategy |
|
||||
|
|
||||
Implement intelligent preloading: |
|
||||
|
|
||||
```typescript |
|
||||
@Component({ |
|
||||
name: 'SmartPreloader' |
|
||||
}) |
|
||||
export default class SmartPreloader extends Vue { |
|
||||
private preloadQueue: Array<() => Promise<any>> = []; |
|
||||
private isPreloading = false; |
|
||||
|
|
||||
// Preload based on user behavior |
|
||||
onUserHover(componentLoader: () => Promise<any>): void { |
|
||||
this.preloadQueue.push(componentLoader); |
|
||||
this.processPreloadQueue(); |
|
||||
} |
|
||||
|
|
||||
private async processPreloadQueue(): Promise<void> { |
|
||||
if (this.isPreloading || this.preloadQueue.length === 0) return; |
|
||||
|
|
||||
this.isPreloading = true; |
|
||||
|
|
||||
while (this.preloadQueue.length > 0) { |
|
||||
const loader = this.preloadQueue.shift(); |
|
||||
if (loader) { |
|
||||
try { |
|
||||
await loader(); |
|
||||
} catch (error) { |
|
||||
console.warn('Preload failed:', error); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
// Small delay between preloads |
|
||||
await new Promise(resolve => setTimeout(resolve, 100)); |
|
||||
} |
|
||||
|
|
||||
this.isPreloading = false; |
|
||||
} |
|
||||
} |
|
||||
``` |
|
||||
|
|
||||
### 4. Bundle Optimization |
|
||||
|
|
||||
Optimize bundle splitting: |
|
||||
|
|
||||
```typescript |
|
||||
// vite.config.ts |
|
||||
export default defineConfig({ |
|
||||
build: { |
|
||||
rollupOptions: { |
|
||||
output: { |
|
||||
manualChunks: (id) => { |
|
||||
// Group by feature |
|
||||
if (id.includes('qr')) return 'qr-feature'; |
|
||||
if (id.includes('three')) return '3d-feature'; |
|
||||
if (id.includes('chart')) return 'chart-feature'; |
|
||||
|
|
||||
// Group by vendor |
|
||||
if (id.includes('node_modules')) { |
|
||||
if (id.includes('vue')) return 'vue-vendor'; |
|
||||
if (id.includes('lodash')) return 'utils-vendor'; |
|
||||
return 'vendor'; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
}); |
|
||||
``` |
|
||||
|
|
||||
## Common Pitfalls |
|
||||
|
|
||||
### 1. Over-Lazy Loading |
|
||||
|
|
||||
❌ **Don't lazy load everything:** |
|
||||
```typescript |
|
||||
// Bad: Lazy loading small components |
|
||||
const SmallButton = defineAsyncComponent(() => import('./SmallButton.vue')); |
|
||||
``` |
|
||||
|
|
||||
✅ **Do lazy load strategically:** |
|
||||
```typescript |
|
||||
// Good: Lazy load heavy components only |
|
||||
const HeavyDataProcessor = defineAsyncComponent(() => import('./HeavyDataProcessor.vue')); |
|
||||
``` |
|
||||
|
|
||||
### 2. Missing Loading States |
|
||||
|
|
||||
❌ **Don't leave users hanging:** |
|
||||
```vue |
|
||||
<template> |
|
||||
<LazyComponent v-if="show" /> |
|
||||
</template> |
|
||||
``` |
|
||||
|
|
||||
✅ **Do provide loading feedback:** |
|
||||
```vue |
|
||||
<template> |
|
||||
<Suspense> |
|
||||
<template #default> |
|
||||
<LazyComponent v-if="show" /> |
|
||||
</template> |
|
||||
<template #fallback> |
|
||||
<LoadingSpinner /> |
|
||||
</template> |
|
||||
</Suspense> |
|
||||
</template> |
|
||||
``` |
|
||||
|
|
||||
### 3. Ignoring Error States |
|
||||
|
|
||||
❌ **Don't ignore loading failures:** |
|
||||
```typescript |
|
||||
const LazyComponent = defineAsyncComponent(() => import('./Component.vue')); |
|
||||
``` |
|
||||
|
|
||||
✅ **Do handle errors gracefully:** |
|
||||
```typescript |
|
||||
const LazyComponent = defineAsyncComponent({ |
|
||||
loader: () => import('./Component.vue'), |
|
||||
errorComponent: ErrorFallback, |
|
||||
onError: (error, retry, fail) => { |
|
||||
console.error('Component load failed:', error); |
|
||||
// Retry once, then fail |
|
||||
retry(); |
|
||||
} |
|
||||
}); |
|
||||
``` |
|
||||
|
|
||||
## Examples |
|
||||
|
|
||||
### Complete Lazy Loading Example |
|
||||
|
|
||||
See the following files for complete working examples: |
|
||||
|
|
||||
1. **`src/components/LazyLoadingExample.vue`** - Main example component |
|
||||
2. **`src/components/sub-components/HeavyComponent.vue`** - Data processing component |
|
||||
3. **`src/components/sub-components/QRScannerComponent.vue`** - Camera-dependent component |
|
||||
4. **`src/components/sub-components/ThreeJSViewer.vue`** - 3D rendering component |
|
||||
5. **`vite.config.optimized.mts`** - Optimized Vite configuration |
|
||||
|
|
||||
### Usage Example |
|
||||
|
|
||||
```vue |
|
||||
<template> |
|
||||
<div class="app"> |
|
||||
<h1>Lazy Loading Demo</h1> |
|
||||
|
|
||||
<LazyLoadingExample |
|
||||
:initial-load-heavy="false" |
|
||||
@qr-detected="handleQRCode" |
|
||||
@model-loaded="handleModelLoaded" |
|
||||
/> |
|
||||
</div> |
|
||||
</template> |
|
||||
|
|
||||
<script lang="ts"> |
|
||||
import { Component, Vue } from 'vue-facing-decorator'; |
|
||||
import LazyLoadingExample from '@/components/LazyLoadingExample.vue'; |
|
||||
|
|
||||
@Component({ |
|
||||
name: 'App', |
|
||||
components: { |
|
||||
LazyLoadingExample |
|
||||
} |
|
||||
}) |
|
||||
export default class App extends Vue { |
|
||||
handleQRCode(data: string): void { |
|
||||
console.log('QR code detected:', data); |
|
||||
} |
|
||||
|
|
||||
handleModelLoaded(info: any): void { |
|
||||
console.log('3D model loaded:', info); |
|
||||
} |
|
||||
} |
|
||||
</script> |
|
||||
``` |
|
||||
|
|
||||
## Conclusion |
|
||||
|
|
||||
Lazy loading with Vue 3 + Vite + `vue-facing-decorator` provides powerful tools for optimizing application performance. By implementing these patterns strategically, you can significantly improve initial load times while maintaining excellent user experience. |
|
||||
|
|
||||
Remember to: |
|
||||
- Use lazy loading for heavy components and features |
|
||||
- Provide meaningful loading and error states |
|
||||
- Monitor performance and bundle sizes |
|
||||
- Implement intelligent preloading strategies |
|
||||
- Handle errors gracefully |
|
||||
|
|
||||
For more information, see the Vue 3 documentation on [Async Components](https://vuejs.org/guide/components/async.html) and the Vite documentation on [Code Splitting](https://vitejs.dev/guide/features.html#code-splitting). |
|
@ -1,554 +0,0 @@ |
|||||
<template> |
|
||||
<div class="heavy-component"> |
|
||||
<h2>Heavy Data Processing Component</h2> |
|
||||
|
|
||||
<!-- Data processing controls --> |
|
||||
<div class="controls"> |
|
||||
<button :disabled="isProcessing" @click="processData"> |
|
||||
{{ isProcessing ? "Processing..." : "Process Data" }} |
|
||||
</button> |
|
||||
<button :disabled="isProcessing" @click="clearResults"> |
|
||||
Clear Results |
|
||||
</button> |
|
||||
</div> |
|
||||
|
|
||||
<!-- Processing status --> |
|
||||
<div v-if="isProcessing" class="processing-status"> |
|
||||
<div class="progress-bar"> |
|
||||
<div class="progress-fill" :style="{ width: progress + '%' }"></div> |
|
||||
</div> |
|
||||
<p>Processing {{ processedCount }} of {{ totalItems }} items...</p> |
|
||||
</div> |
|
||||
|
|
||||
<!-- Results display --> |
|
||||
<div v-if="processedData.length > 0" class="results"> |
|
||||
<h3>Processed Results ({{ processedData.length }} items)</h3> |
|
||||
|
|
||||
<!-- Filter controls --> |
|
||||
<div class="filters"> |
|
||||
<input |
|
||||
v-model="searchTerm" |
|
||||
placeholder="Search items..." |
|
||||
class="search-input" |
|
||||
/> |
|
||||
<select v-model="sortBy" class="sort-select"> |
|
||||
<option value="name">Sort by Name</option> |
|
||||
<option value="id">Sort by ID</option> |
|
||||
<option value="processed">Sort by Processed Date</option> |
|
||||
</select> |
|
||||
</div> |
|
||||
|
|
||||
<!-- Results list --> |
|
||||
<div class="results-list"> |
|
||||
<div |
|
||||
v-for="item in filteredAndSortedData" |
|
||||
:key="item.id" |
|
||||
class="result-item" |
|
||||
> |
|
||||
<div class="item-header"> |
|
||||
<span class="item-name">{{ item.name }}</span> |
|
||||
<span class="item-id">#{{ item.id }}</span> |
|
||||
</div> |
|
||||
<div class="item-details"> |
|
||||
<span class="processed-date"> |
|
||||
Processed: {{ formatDate(item.processedAt) }} |
|
||||
</span> |
|
||||
<span class="processing-time"> |
|
||||
Time: {{ item.processingTime }}ms |
|
||||
</span> |
|
||||
</div> |
|
||||
</div> |
|
||||
</div> |
|
||||
|
|
||||
<!-- Pagination --> |
|
||||
<div v-if="totalPages > 1" class="pagination"> |
|
||||
<button |
|
||||
:disabled="currentPage === 1" |
|
||||
class="page-btn" |
|
||||
@click="previousPage" |
|
||||
> |
|
||||
Previous |
|
||||
</button> |
|
||||
<span class="page-info"> |
|
||||
Page {{ currentPage }} of {{ totalPages }} |
|
||||
</span> |
|
||||
<button |
|
||||
:disabled="currentPage === totalPages" |
|
||||
class="page-btn" |
|
||||
@click="nextPage" |
|
||||
> |
|
||||
Next |
|
||||
</button> |
|
||||
</div> |
|
||||
</div> |
|
||||
|
|
||||
<!-- Performance metrics --> |
|
||||
<div v-if="performanceMetrics" class="performance-metrics"> |
|
||||
<h4>Performance Metrics</h4> |
|
||||
<div class="metrics-grid"> |
|
||||
<div class="metric"> |
|
||||
<span class="metric-label">Total Processing Time:</span> |
|
||||
<span class="metric-value">{{ performanceMetrics.totalTime }}ms</span> |
|
||||
</div> |
|
||||
<div class="metric"> |
|
||||
<span class="metric-label">Average per Item:</span> |
|
||||
<span class="metric-value" |
|
||||
>{{ performanceMetrics.averageTime }}ms</span |
|
||||
> |
|
||||
</div> |
|
||||
<div class="metric"> |
|
||||
<span class="metric-label">Memory Usage:</span> |
|
||||
<span class="metric-value" |
|
||||
>{{ performanceMetrics.memoryUsage }}MB</span |
|
||||
> |
|
||||
</div> |
|
||||
</div> |
|
||||
</div> |
|
||||
</div> |
|
||||
</template> |
|
||||
|
|
||||
<script lang="ts"> |
|
||||
import { Component, Vue, Prop, Emit } from "vue-facing-decorator"; |
|
||||
|
|
||||
interface ProcessedItem { |
|
||||
id: number; |
|
||||
name: string; |
|
||||
processedAt: Date; |
|
||||
processingTime: number; |
|
||||
result: any; |
|
||||
} |
|
||||
|
|
||||
interface PerformanceMetrics { |
|
||||
totalTime: number; |
|
||||
averageTime: number; |
|
||||
memoryUsage: number; |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* Heavy Component for Data Processing |
|
||||
* |
|
||||
* Demonstrates a component that performs intensive data processing |
|
||||
* and would benefit from lazy loading to avoid blocking the main thread. |
|
||||
* |
|
||||
* @author Matthew Raymer |
|
||||
* @version 1.0.0 |
|
||||
*/ |
|
||||
@Component({ |
|
||||
name: "HeavyComponent", |
|
||||
}) |
|
||||
export default class HeavyComponent extends Vue { |
|
||||
@Prop({ required: true }) readonly data!: { |
|
||||
items: Array<{ id: number; name: string }>; |
|
||||
filters: Record<string, any>; |
|
||||
sortBy: string; |
|
||||
}; |
|
||||
|
|
||||
// Component state |
|
||||
isProcessing = false; |
|
||||
processedData: ProcessedItem[] = []; |
|
||||
progress = 0; |
|
||||
processedCount = 0; |
|
||||
totalItems = 0; |
|
||||
|
|
||||
// UI state |
|
||||
searchTerm = ""; |
|
||||
sortBy = "name"; |
|
||||
currentPage = 1; |
|
||||
itemsPerPage = 50; |
|
||||
|
|
||||
// Performance tracking |
|
||||
performanceMetrics: PerformanceMetrics | null = null; |
|
||||
startTime = 0; |
|
||||
|
|
||||
// Computed properties |
|
||||
get filteredAndSortedData(): ProcessedItem[] { |
|
||||
let filtered = this.processedData; |
|
||||
|
|
||||
// Apply search filter |
|
||||
if (this.searchTerm) { |
|
||||
filtered = filtered.filter((item) => |
|
||||
item.name.toLowerCase().includes(this.searchTerm.toLowerCase()), |
|
||||
); |
|
||||
} |
|
||||
|
|
||||
// Apply sorting |
|
||||
filtered.sort((a, b) => { |
|
||||
switch (this.sortBy) { |
|
||||
case "name": |
|
||||
return a.name.localeCompare(b.name); |
|
||||
case "id": |
|
||||
return a.id - b.id; |
|
||||
case "processed": |
|
||||
return b.processedAt.getTime() - a.processedAt.getTime(); |
|
||||
default: |
|
||||
return 0; |
|
||||
} |
|
||||
}); |
|
||||
|
|
||||
return filtered; |
|
||||
} |
|
||||
|
|
||||
get paginatedData(): ProcessedItem[] { |
|
||||
const start = (this.currentPage - 1) * this.itemsPerPage; |
|
||||
const end = start + this.itemsPerPage; |
|
||||
return this.filteredAndSortedData.slice(start, end); |
|
||||
} |
|
||||
|
|
||||
get totalPages(): number { |
|
||||
return Math.ceil(this.filteredAndSortedData.length / this.itemsPerPage); |
|
||||
} |
|
||||
|
|
||||
// Lifecycle hooks |
|
||||
mounted(): void { |
|
||||
console.log( |
|
||||
"[HeavyComponent] Component mounted with", |
|
||||
this.data.items.length, |
|
||||
"items", |
|
||||
); |
|
||||
this.totalItems = this.data.items.length; |
|
||||
} |
|
||||
|
|
||||
// Methods |
|
||||
async processData(): Promise<void> { |
|
||||
if (this.isProcessing) return; |
|
||||
|
|
||||
this.isProcessing = true; |
|
||||
this.progress = 0; |
|
||||
this.processedCount = 0; |
|
||||
this.processedData = []; |
|
||||
this.startTime = performance.now(); |
|
||||
|
|
||||
console.log("[HeavyComponent] Starting data processing..."); |
|
||||
|
|
||||
try { |
|
||||
// Process items in batches to avoid blocking the UI |
|
||||
const batchSize = 10; |
|
||||
const items = this.data.items; |
|
||||
|
|
||||
for (let i = 0; i < items.length; i += batchSize) { |
|
||||
const batch = items.slice(i, i + batchSize); |
|
||||
|
|
||||
// Process batch |
|
||||
await this.processBatch(batch); |
|
||||
|
|
||||
// Update progress |
|
||||
this.processedCount = Math.min(i + batchSize, items.length); |
|
||||
this.progress = (this.processedCount / items.length) * 100; |
|
||||
|
|
||||
// Allow UI to update |
|
||||
await this.$nextTick(); |
|
||||
|
|
||||
// Small delay to prevent overwhelming the UI |
|
||||
await new Promise((resolve) => setTimeout(resolve, 10)); |
|
||||
} |
|
||||
|
|
||||
// Calculate performance metrics |
|
||||
this.calculatePerformanceMetrics(); |
|
||||
|
|
||||
// Emit completion event |
|
||||
this.$emit("data-processed", { |
|
||||
totalItems: this.processedData.length, |
|
||||
processingTime: performance.now() - this.startTime, |
|
||||
metrics: this.performanceMetrics, |
|
||||
}); |
|
||||
|
|
||||
console.log("[HeavyComponent] Data processing completed"); |
|
||||
} catch (error) { |
|
||||
console.error("[HeavyComponent] Processing error:", error); |
|
||||
this.$emit("processing-error", error); |
|
||||
} finally { |
|
||||
this.isProcessing = false; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
private async processBatch( |
|
||||
batch: Array<{ id: number; name: string }>, |
|
||||
): Promise<void> { |
|
||||
const processedBatch = await Promise.all( |
|
||||
batch.map(async (item) => { |
|
||||
const itemStartTime = performance.now(); |
|
||||
|
|
||||
// Simulate heavy processing |
|
||||
await this.simulateHeavyProcessing(item); |
|
||||
|
|
||||
const processingTime = performance.now() - itemStartTime; |
|
||||
|
|
||||
return { |
|
||||
id: item.id, |
|
||||
name: item.name, |
|
||||
processedAt: new Date(), |
|
||||
processingTime: Math.round(processingTime), |
|
||||
result: this.generateResult(item), |
|
||||
}; |
|
||||
}), |
|
||||
); |
|
||||
|
|
||||
this.processedData.push(...processedBatch); |
|
||||
} |
|
||||
|
|
||||
private async simulateHeavyProcessing(item: { |
|
||||
id: number; |
|
||||
name: string; |
|
||||
}): Promise<void> { |
|
||||
// Simulate CPU-intensive work |
|
||||
const complexity = item.name.length * item.id; |
|
||||
const iterations = Math.min(complexity, 1000); // Cap at 1000 iterations |
|
||||
|
|
||||
for (let i = 0; i < iterations; i++) { |
|
||||
// Simulate work |
|
||||
Math.sqrt(i) * Math.random(); |
|
||||
} |
|
||||
|
|
||||
// Simulate async work |
|
||||
await new Promise((resolve) => setTimeout(resolve, Math.random() * 10)); |
|
||||
} |
|
||||
|
|
||||
private generateResult(item: { id: number; name: string }): any { |
|
||||
return { |
|
||||
hash: this.generateHash(item.name + item.id), |
|
||||
category: this.categorizeItem(item), |
|
||||
score: Math.random() * 100, |
|
||||
tags: this.generateTags(item), |
|
||||
}; |
|
||||
} |
|
||||
|
|
||||
private generateHash(input: string): string { |
|
||||
let hash = 0; |
|
||||
for (let i = 0; i < input.length; i++) { |
|
||||
const char = input.charCodeAt(i); |
|
||||
hash = (hash << 5) - hash + char; |
|
||||
hash = hash & hash; // Convert to 32-bit integer |
|
||||
} |
|
||||
return hash.toString(16); |
|
||||
} |
|
||||
|
|
||||
private categorizeItem(item: { id: number; name: string }): string { |
|
||||
const categories = ["A", "B", "C", "D", "E"]; |
|
||||
return categories[item.id % categories.length]; |
|
||||
} |
|
||||
|
|
||||
private generateTags(item: { id: number; name: string }): string[] { |
|
||||
const tags = ["important", "urgent", "review", "archive", "featured"]; |
|
||||
return tags.filter((_, index) => (item.id + index) % 3 === 0); |
|
||||
} |
|
||||
|
|
||||
private calculatePerformanceMetrics(): void { |
|
||||
const totalTime = performance.now() - this.startTime; |
|
||||
const averageTime = totalTime / this.processedData.length; |
|
||||
|
|
||||
// Simulate memory usage calculation |
|
||||
const memoryUsage = this.processedData.length * 0.1; // 0.1MB per item |
|
||||
|
|
||||
this.performanceMetrics = { |
|
||||
totalTime: Math.round(totalTime), |
|
||||
averageTime: Math.round(averageTime), |
|
||||
memoryUsage: Math.round(memoryUsage * 100) / 100, |
|
||||
}; |
|
||||
} |
|
||||
|
|
||||
clearResults(): void { |
|
||||
this.processedData = []; |
|
||||
this.performanceMetrics = null; |
|
||||
this.searchTerm = ""; |
|
||||
this.currentPage = 1; |
|
||||
console.log("[HeavyComponent] Results cleared"); |
|
||||
} |
|
||||
|
|
||||
previousPage(): void { |
|
||||
if (this.currentPage > 1) { |
|
||||
this.currentPage--; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
nextPage(): void { |
|
||||
if (this.currentPage < this.totalPages) { |
|
||||
this.currentPage++; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
formatDate(date: Date): string { |
|
||||
return date.toLocaleString(); |
|
||||
} |
|
||||
|
|
||||
// Event emitters |
|
||||
@Emit("data-processed") |
|
||||
emitDataProcessed(data: any): any { |
|
||||
return data; |
|
||||
} |
|
||||
|
|
||||
@Emit("processing-error") |
|
||||
emitProcessingError(error: Error): Error { |
|
||||
return error; |
|
||||
} |
|
||||
} |
|
||||
</script> |
|
||||
|
|
||||
<style scoped> |
|
||||
.heavy-component { |
|
||||
padding: 20px; |
|
||||
border: 1px solid #ddd; |
|
||||
border-radius: 8px; |
|
||||
background: #f9f9f9; |
|
||||
} |
|
||||
|
|
||||
.controls { |
|
||||
display: flex; |
|
||||
gap: 10px; |
|
||||
margin-bottom: 20px; |
|
||||
} |
|
||||
|
|
||||
.controls button { |
|
||||
padding: 8px 16px; |
|
||||
border: 1px solid #ccc; |
|
||||
border-radius: 4px; |
|
||||
background: #fff; |
|
||||
cursor: pointer; |
|
||||
transition: background-color 0.2s; |
|
||||
} |
|
||||
|
|
||||
.controls button:hover:not(:disabled) { |
|
||||
background: #e9ecef; |
|
||||
} |
|
||||
|
|
||||
.controls button:disabled { |
|
||||
opacity: 0.6; |
|
||||
cursor: not-allowed; |
|
||||
} |
|
||||
|
|
||||
.processing-status { |
|
||||
margin-bottom: 20px; |
|
||||
} |
|
||||
|
|
||||
.progress-bar { |
|
||||
width: 100%; |
|
||||
height: 20px; |
|
||||
background: #e9ecef; |
|
||||
border-radius: 10px; |
|
||||
overflow: hidden; |
|
||||
margin-bottom: 10px; |
|
||||
} |
|
||||
|
|
||||
.progress-fill { |
|
||||
height: 100%; |
|
||||
background: linear-gradient(90deg, #007bff, #0056b3); |
|
||||
transition: width 0.3s ease; |
|
||||
} |
|
||||
|
|
||||
.results { |
|
||||
margin-top: 20px; |
|
||||
} |
|
||||
|
|
||||
.filters { |
|
||||
display: flex; |
|
||||
gap: 10px; |
|
||||
margin-bottom: 20px; |
|
||||
} |
|
||||
|
|
||||
.search-input, |
|
||||
.sort-select { |
|
||||
padding: 8px; |
|
||||
border: 1px solid #ccc; |
|
||||
border-radius: 4px; |
|
||||
} |
|
||||
|
|
||||
.search-input { |
|
||||
flex: 1; |
|
||||
} |
|
||||
|
|
||||
.results-list { |
|
||||
max-height: 400px; |
|
||||
overflow-y: auto; |
|
||||
border: 1px solid #ddd; |
|
||||
border-radius: 4px; |
|
||||
background: #fff; |
|
||||
} |
|
||||
|
|
||||
.result-item { |
|
||||
padding: 12px; |
|
||||
border-bottom: 1px solid #eee; |
|
||||
} |
|
||||
|
|
||||
.result-item:last-child { |
|
||||
border-bottom: none; |
|
||||
} |
|
||||
|
|
||||
.item-header { |
|
||||
display: flex; |
|
||||
justify-content: space-between; |
|
||||
margin-bottom: 8px; |
|
||||
} |
|
||||
|
|
||||
.item-name { |
|
||||
font-weight: bold; |
|
||||
} |
|
||||
|
|
||||
.item-id { |
|
||||
color: #666; |
|
||||
font-size: 0.9em; |
|
||||
} |
|
||||
|
|
||||
.item-details { |
|
||||
display: flex; |
|
||||
gap: 20px; |
|
||||
font-size: 0.85em; |
|
||||
color: #666; |
|
||||
} |
|
||||
|
|
||||
.pagination { |
|
||||
display: flex; |
|
||||
justify-content: center; |
|
||||
align-items: center; |
|
||||
gap: 15px; |
|
||||
margin-top: 20px; |
|
||||
} |
|
||||
|
|
||||
.page-btn { |
|
||||
padding: 6px 12px; |
|
||||
border: 1px solid #ccc; |
|
||||
border-radius: 4px; |
|
||||
background: #fff; |
|
||||
cursor: pointer; |
|
||||
} |
|
||||
|
|
||||
.page-btn:disabled { |
|
||||
opacity: 0.5; |
|
||||
cursor: not-allowed; |
|
||||
} |
|
||||
|
|
||||
.page-info { |
|
||||
font-size: 0.9em; |
|
||||
color: #666; |
|
||||
} |
|
||||
|
|
||||
.performance-metrics { |
|
||||
margin-top: 20px; |
|
||||
padding: 15px; |
|
||||
background: #e8f4fd; |
|
||||
border-radius: 4px; |
|
||||
} |
|
||||
|
|
||||
.metrics-grid { |
|
||||
display: grid; |
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); |
|
||||
gap: 15px; |
|
||||
margin-top: 10px; |
|
||||
} |
|
||||
|
|
||||
.metric { |
|
||||
display: flex; |
|
||||
justify-content: space-between; |
|
||||
padding: 8px; |
|
||||
background: #fff; |
|
||||
border-radius: 4px; |
|
||||
} |
|
||||
|
|
||||
.metric-label { |
|
||||
font-weight: bold; |
|
||||
color: #333; |
|
||||
} |
|
||||
|
|
||||
.metric-value { |
|
||||
color: #007bff; |
|
||||
font-weight: bold; |
|
||||
} |
|
||||
</style> |
|
@ -1,721 +0,0 @@ |
|||||
<template> |
|
||||
<div class="qr-scanner-component"> |
|
||||
<h2>QR Code Scanner</h2> |
|
||||
|
|
||||
<!-- Camera controls --> |
|
||||
<div class="camera-controls"> |
|
||||
<button :disabled="isScanning || !hasCamera" @click="startScanning"> |
|
||||
{{ isScanning ? "Scanning..." : "Start Scanning" }} |
|
||||
</button> |
|
||||
<button :disabled="!isScanning" @click="stopScanning"> |
|
||||
Stop Scanning |
|
||||
</button> |
|
||||
<button |
|
||||
:disabled="!isScanning || cameras.length <= 1" |
|
||||
@click="switchCamera" |
|
||||
> |
|
||||
Switch Camera |
|
||||
</button> |
|
||||
</div> |
|
||||
|
|
||||
<!-- Camera status --> |
|
||||
<div class="camera-status"> |
|
||||
<div v-if="!hasCamera" class="status-error"> |
|
||||
<p>Camera not available</p> |
|
||||
<p class="status-detail"> |
|
||||
This device doesn't have a camera or camera access is denied. |
|
||||
</p> |
|
||||
</div> |
|
||||
|
|
||||
<div v-else-if="!isScanning" class="status-info"> |
|
||||
<p>Camera ready</p> |
|
||||
<p class="status-detail"> |
|
||||
Click "Start Scanning" to begin QR code detection. |
|
||||
</p> |
|
||||
</div> |
|
||||
|
|
||||
<div v-else class="status-scanning"> |
|
||||
<p>Scanning for QR codes...</p> |
|
||||
<p class="status-detail">Point camera at a QR code to scan.</p> |
|
||||
</div> |
|
||||
</div> |
|
||||
|
|
||||
<!-- Camera view --> |
|
||||
<div v-if="isScanning && hasCamera" class="camera-container"> |
|
||||
<video |
|
||||
ref="videoElement" |
|
||||
class="camera-video" |
|
||||
autoplay |
|
||||
playsinline |
|
||||
muted |
|
||||
></video> |
|
||||
|
|
||||
<!-- Scanning overlay --> |
|
||||
<div class="scanning-overlay"> |
|
||||
<div class="scan-frame"></div> |
|
||||
<div class="scan-line"></div> |
|
||||
</div> |
|
||||
</div> |
|
||||
|
|
||||
<!-- Scan results --> |
|
||||
<div v-if="scanResults.length > 0" class="scan-results"> |
|
||||
<h3>Scan Results ({{ scanResults.length }})</h3> |
|
||||
|
|
||||
<div class="results-list"> |
|
||||
<div |
|
||||
v-for="(result, index) in scanResults" |
|
||||
:key="index" |
|
||||
class="result-item" |
|
||||
> |
|
||||
<div class="result-header"> |
|
||||
<span class="result-number">#{{ index + 1 }}</span> |
|
||||
<span class="result-time">{{ formatTime(result.timestamp) }}</span> |
|
||||
</div> |
|
||||
<div class="result-content"> |
|
||||
<div class="qr-data"><strong>Data:</strong> {{ result.data }}</div> |
|
||||
<div class="qr-format"> |
|
||||
<strong>Format:</strong> {{ result.format }} |
|
||||
</div> |
|
||||
</div> |
|
||||
<div class="result-actions"> |
|
||||
<button class="copy-btn" @click="copyToClipboard(result.data)"> |
|
||||
Copy |
|
||||
</button> |
|
||||
<button class="remove-btn" @click="removeResult(index)"> |
|
||||
Remove |
|
||||
</button> |
|
||||
</div> |
|
||||
</div> |
|
||||
</div> |
|
||||
|
|
||||
<div class="results-actions"> |
|
||||
<button class="clear-btn" @click="clearResults"> |
|
||||
Clear All Results |
|
||||
</button> |
|
||||
<button class="export-btn" @click="exportResults"> |
|
||||
Export Results |
|
||||
</button> |
|
||||
</div> |
|
||||
</div> |
|
||||
|
|
||||
<!-- Settings panel --> |
|
||||
<div class="settings-panel"> |
|
||||
<h3>Scanner Settings</h3> |
|
||||
|
|
||||
<div class="setting-group"> |
|
||||
<label> |
|
||||
<input v-model="settings.continuousScanning" type="checkbox" /> |
|
||||
Continuous Scanning |
|
||||
</label> |
|
||||
<p class="setting-description"> |
|
||||
Automatically scan multiple QR codes without stopping |
|
||||
</p> |
|
||||
</div> |
|
||||
|
|
||||
<div class="setting-group"> |
|
||||
<label> |
|
||||
<input v-model="settings.audioFeedback" type="checkbox" /> |
|
||||
Audio Feedback |
|
||||
</label> |
|
||||
<p class="setting-description">Play sound when QR code is detected</p> |
|
||||
</div> |
|
||||
|
|
||||
<div class="setting-group"> |
|
||||
<label> |
|
||||
<input v-model="settings.vibrateOnScan" type="checkbox" /> |
|
||||
Vibration Feedback |
|
||||
</label> |
|
||||
<p class="setting-description"> |
|
||||
Vibrate device when QR code is detected |
|
||||
</p> |
|
||||
</div> |
|
||||
|
|
||||
<div class="setting-group"> |
|
||||
<label>Scan Interval (ms):</label> |
|
||||
<input |
|
||||
v-model.number="settings.scanInterval" |
|
||||
type="number" |
|
||||
min="100" |
|
||||
max="5000" |
|
||||
step="100" |
|
||||
/> |
|
||||
<p class="setting-description"> |
|
||||
Time between scans (lower = faster, higher = more accurate) |
|
||||
</p> |
|
||||
</div> |
|
||||
</div> |
|
||||
</div> |
|
||||
</template> |
|
||||
|
|
||||
<script lang="ts"> |
|
||||
import { Component, Vue, Emit } from "vue-facing-decorator"; |
|
||||
|
|
||||
interface ScanResult { |
|
||||
data: string; |
|
||||
format: string; |
|
||||
timestamp: Date; |
|
||||
} |
|
||||
|
|
||||
interface ScannerSettings { |
|
||||
continuousScanning: boolean; |
|
||||
audioFeedback: boolean; |
|
||||
vibrateOnScan: boolean; |
|
||||
scanInterval: number; |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* QR Scanner Component |
|
||||
* |
|
||||
* Demonstrates lazy loading for camera-dependent features. |
|
||||
* This component would benefit from lazy loading as it requires |
|
||||
* camera permissions and heavy camera processing libraries. |
|
||||
* |
|
||||
* @author Matthew Raymer |
|
||||
* @version 1.0.0 |
|
||||
*/ |
|
||||
@Component({ |
|
||||
name: "QRScannerComponent", |
|
||||
}) |
|
||||
export default class QRScannerComponent extends Vue { |
|
||||
// Component state |
|
||||
isScanning = false; |
|
||||
hasCamera = false; |
|
||||
cameras: MediaDeviceInfo[] = []; |
|
||||
currentCameraIndex = 0; |
|
||||
|
|
||||
// Video element reference |
|
||||
videoElement: HTMLVideoElement | null = null; |
|
||||
|
|
||||
// Scan results |
|
||||
scanResults: ScanResult[] = []; |
|
||||
|
|
||||
// Scanner settings |
|
||||
settings: ScannerSettings = { |
|
||||
continuousScanning: true, |
|
||||
audioFeedback: true, |
|
||||
vibrateOnScan: true, |
|
||||
scanInterval: 500, |
|
||||
}; |
|
||||
|
|
||||
// Internal state |
|
||||
private stream: MediaStream | null = null; |
|
||||
private scanInterval: number | null = null; |
|
||||
private lastScanTime = 0; |
|
||||
|
|
||||
// Lifecycle hooks |
|
||||
async mounted(): Promise<void> { |
|
||||
console.log("[QRScannerComponent] Component mounted"); |
|
||||
await this.initializeCamera(); |
|
||||
} |
|
||||
|
|
||||
beforeUnmount(): void { |
|
||||
this.stopScanning(); |
|
||||
console.log("[QRScannerComponent] Component unmounting"); |
|
||||
} |
|
||||
|
|
||||
// Methods |
|
||||
async initializeCamera(): Promise<void> { |
|
||||
try { |
|
||||
// Check if camera is available |
|
||||
const devices = await navigator.mediaDevices.enumerateDevices(); |
|
||||
this.cameras = devices.filter((device) => device.kind === "videoinput"); |
|
||||
this.hasCamera = this.cameras.length > 0; |
|
||||
|
|
||||
if (this.hasCamera) { |
|
||||
console.log( |
|
||||
"[QRScannerComponent] Camera available:", |
|
||||
this.cameras.length, |
|
||||
"devices", |
|
||||
); |
|
||||
} else { |
|
||||
console.warn("[QRScannerComponent] No camera devices found"); |
|
||||
} |
|
||||
} catch (error) { |
|
||||
console.error("[QRScannerComponent] Camera initialization error:", error); |
|
||||
this.hasCamera = false; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
async startScanning(): Promise<void> { |
|
||||
if (!this.hasCamera || this.isScanning) return; |
|
||||
|
|
||||
try { |
|
||||
console.log("[QRScannerComponent] Starting QR scanning..."); |
|
||||
|
|
||||
// Get camera stream |
|
||||
const constraints = { |
|
||||
video: { |
|
||||
deviceId: this.cameras[this.currentCameraIndex]?.deviceId, |
|
||||
}, |
|
||||
}; |
|
||||
|
|
||||
this.stream = await navigator.mediaDevices.getUserMedia(constraints); |
|
||||
|
|
||||
// Set up video element |
|
||||
this.videoElement = this.$refs.videoElement as HTMLVideoElement; |
|
||||
if (this.videoElement) { |
|
||||
this.videoElement.srcObject = this.stream; |
|
||||
await this.videoElement.play(); |
|
||||
} |
|
||||
|
|
||||
this.isScanning = true; |
|
||||
|
|
||||
// Start QR code detection |
|
||||
this.startQRDetection(); |
|
||||
|
|
||||
console.log("[QRScannerComponent] QR scanning started"); |
|
||||
} catch (error) { |
|
||||
console.error("[QRScannerComponent] Failed to start scanning:", error); |
|
||||
this.hasCamera = false; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
stopScanning(): void { |
|
||||
if (!this.isScanning) return; |
|
||||
|
|
||||
console.log("[QRScannerComponent] Stopping QR scanning..."); |
|
||||
|
|
||||
// Stop QR detection |
|
||||
this.stopQRDetection(); |
|
||||
|
|
||||
// Stop camera stream |
|
||||
if (this.stream) { |
|
||||
this.stream.getTracks().forEach((track) => track.stop()); |
|
||||
this.stream = null; |
|
||||
} |
|
||||
|
|
||||
// Clear video element |
|
||||
if (this.videoElement) { |
|
||||
this.videoElement.srcObject = null; |
|
||||
this.videoElement = null; |
|
||||
} |
|
||||
|
|
||||
this.isScanning = false; |
|
||||
console.log("[QRScannerComponent] QR scanning stopped"); |
|
||||
} |
|
||||
|
|
||||
async switchCamera(): Promise<void> { |
|
||||
if (this.cameras.length <= 1) return; |
|
||||
|
|
||||
// Stop current scanning |
|
||||
this.stopScanning(); |
|
||||
|
|
||||
// Switch to next camera |
|
||||
this.currentCameraIndex = |
|
||||
(this.currentCameraIndex + 1) % this.cameras.length; |
|
||||
|
|
||||
// Restart scanning with new camera |
|
||||
await this.startScanning(); |
|
||||
|
|
||||
console.log( |
|
||||
"[QRScannerComponent] Switched to camera:", |
|
||||
this.currentCameraIndex, |
|
||||
); |
|
||||
} |
|
||||
|
|
||||
private startQRDetection(): void { |
|
||||
if (!this.settings.continuousScanning) return; |
|
||||
|
|
||||
this.scanInterval = window.setInterval(() => { |
|
||||
this.detectQRCode(); |
|
||||
}, this.settings.scanInterval); |
|
||||
} |
|
||||
|
|
||||
private stopQRDetection(): void { |
|
||||
if (this.scanInterval) { |
|
||||
clearInterval(this.scanInterval); |
|
||||
this.scanInterval = null; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
private async detectQRCode(): Promise<void> { |
|
||||
if (!this.videoElement || !this.isScanning) return; |
|
||||
|
|
||||
const now = Date.now(); |
|
||||
if (now - this.lastScanTime < this.settings.scanInterval) return; |
|
||||
|
|
||||
try { |
|
||||
// Simulate QR code detection |
|
||||
// In a real implementation, you would use a QR code library like jsQR |
|
||||
const detectedQR = await this.simulateQRDetection(); |
|
||||
|
|
||||
if (detectedQR) { |
|
||||
this.addScanResult(detectedQR); |
|
||||
this.lastScanTime = now; |
|
||||
} |
|
||||
} catch (error) { |
|
||||
console.error("[QRScannerComponent] QR detection error:", error); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
private async simulateQRDetection(): Promise<ScanResult | null> { |
|
||||
// Simulate QR code detection with random chance |
|
||||
if (Math.random() < 0.1) { |
|
||||
// 10% chance of detection |
|
||||
const sampleData = [ |
|
||||
"https://example.com/qr1", |
|
||||
"WIFI:S:MyNetwork;T:WPA;P:password123;;", |
|
||||
"BEGIN:VCARD\nVERSION:3.0\nFN:John Doe\nTEL:+1234567890\nEND:VCARD", |
|
||||
"otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&issuer=Example", |
|
||||
]; |
|
||||
|
|
||||
const formats = ["URL", "WiFi", "vCard", "TOTP"]; |
|
||||
const randomIndex = Math.floor(Math.random() * sampleData.length); |
|
||||
|
|
||||
return { |
|
||||
data: sampleData[randomIndex], |
|
||||
format: formats[randomIndex], |
|
||||
timestamp: new Date(), |
|
||||
}; |
|
||||
} |
|
||||
|
|
||||
return null; |
|
||||
} |
|
||||
|
|
||||
private addScanResult(result: ScanResult): void { |
|
||||
// Check for duplicates |
|
||||
const isDuplicate = this.scanResults.some( |
|
||||
(existing) => existing.data === result.data, |
|
||||
); |
|
||||
|
|
||||
if (!isDuplicate) { |
|
||||
this.scanResults.unshift(result); |
|
||||
|
|
||||
// Provide feedback |
|
||||
this.provideFeedback(); |
|
||||
|
|
||||
// Emit event |
|
||||
this.$emit("qr-detected", result.data); |
|
||||
|
|
||||
console.log("[QRScannerComponent] QR code detected:", result.data); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
private provideFeedback(): void { |
|
||||
// Audio feedback |
|
||||
if (this.settings.audioFeedback) { |
|
||||
this.playBeepSound(); |
|
||||
} |
|
||||
|
|
||||
// Vibration feedback |
|
||||
if (this.settings.vibrateOnScan && "vibrate" in navigator) { |
|
||||
navigator.vibrate(100); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
private playBeepSound(): void { |
|
||||
// Create a simple beep sound |
|
||||
const audioContext = new (window.AudioContext || |
|
||||
(window as any).webkitAudioContext)(); |
|
||||
const oscillator = audioContext.createOscillator(); |
|
||||
const gainNode = audioContext.createGain(); |
|
||||
|
|
||||
oscillator.connect(gainNode); |
|
||||
gainNode.connect(audioContext.destination); |
|
||||
|
|
||||
oscillator.frequency.setValueAtTime(800, audioContext.currentTime); |
|
||||
oscillator.type = "sine"; |
|
||||
|
|
||||
gainNode.gain.setValueAtTime(0.1, audioContext.currentTime); |
|
||||
gainNode.gain.exponentialRampToValueAtTime( |
|
||||
0.01, |
|
||||
audioContext.currentTime + 0.1, |
|
||||
); |
|
||||
|
|
||||
oscillator.start(audioContext.currentTime); |
|
||||
oscillator.stop(audioContext.currentTime + 0.1); |
|
||||
} |
|
||||
|
|
||||
copyToClipboard(text: string): void { |
|
||||
navigator.clipboard |
|
||||
.writeText(text) |
|
||||
.then(() => { |
|
||||
console.log("[QRScannerComponent] Copied to clipboard:", text); |
|
||||
}) |
|
||||
.catch((error) => { |
|
||||
console.error("[QRScannerComponent] Failed to copy:", error); |
|
||||
}); |
|
||||
} |
|
||||
|
|
||||
removeResult(index: number): void { |
|
||||
this.scanResults.splice(index, 1); |
|
||||
} |
|
||||
|
|
||||
clearResults(): void { |
|
||||
this.scanResults = []; |
|
||||
console.log("[QRScannerComponent] Results cleared"); |
|
||||
} |
|
||||
|
|
||||
exportResults(): void { |
|
||||
const data = JSON.stringify(this.scanResults, null, 2); |
|
||||
const blob = new Blob([data], { type: "application/json" }); |
|
||||
const url = URL.createObjectURL(blob); |
|
||||
|
|
||||
const a = document.createElement("a"); |
|
||||
a.href = url; |
|
||||
a.download = `qr-scan-results-${new Date().toISOString().split("T")[0]}.json`; |
|
||||
a.click(); |
|
||||
|
|
||||
URL.revokeObjectURL(url); |
|
||||
console.log("[QRScannerComponent] Results exported"); |
|
||||
} |
|
||||
|
|
||||
formatTime(date: Date): string { |
|
||||
return date.toLocaleTimeString(); |
|
||||
} |
|
||||
|
|
||||
// Event emitters |
|
||||
@Emit("qr-detected") |
|
||||
emitQRDetected(data: string): string { |
|
||||
return data; |
|
||||
} |
|
||||
} |
|
||||
</script> |
|
||||
|
|
||||
<style scoped> |
|
||||
.qr-scanner-component { |
|
||||
padding: 20px; |
|
||||
border: 1px solid #ddd; |
|
||||
border-radius: 8px; |
|
||||
background: #f9f9f9; |
|
||||
} |
|
||||
|
|
||||
.camera-controls { |
|
||||
display: flex; |
|
||||
gap: 10px; |
|
||||
margin-bottom: 20px; |
|
||||
flex-wrap: wrap; |
|
||||
} |
|
||||
|
|
||||
.camera-controls button { |
|
||||
padding: 8px 16px; |
|
||||
border: 1px solid #ccc; |
|
||||
border-radius: 4px; |
|
||||
background: #fff; |
|
||||
cursor: pointer; |
|
||||
transition: background-color 0.2s; |
|
||||
} |
|
||||
|
|
||||
.camera-controls button:hover:not(:disabled) { |
|
||||
background: #e9ecef; |
|
||||
} |
|
||||
|
|
||||
.camera-controls button:disabled { |
|
||||
opacity: 0.6; |
|
||||
cursor: not-allowed; |
|
||||
} |
|
||||
|
|
||||
.camera-status { |
|
||||
margin-bottom: 20px; |
|
||||
padding: 15px; |
|
||||
border-radius: 4px; |
|
||||
} |
|
||||
|
|
||||
.status-error { |
|
||||
background: #f8d7da; |
|
||||
border: 1px solid #f5c6cb; |
|
||||
color: #721c24; |
|
||||
} |
|
||||
|
|
||||
.status-info { |
|
||||
background: #d1ecf1; |
|
||||
border: 1px solid #bee5eb; |
|
||||
color: #0c5460; |
|
||||
} |
|
||||
|
|
||||
.status-scanning { |
|
||||
background: #d4edda; |
|
||||
border: 1px solid #c3e6cb; |
|
||||
color: #155724; |
|
||||
} |
|
||||
|
|
||||
.status-detail { |
|
||||
margin-top: 5px; |
|
||||
font-size: 0.9em; |
|
||||
opacity: 0.8; |
|
||||
} |
|
||||
|
|
||||
.camera-container { |
|
||||
position: relative; |
|
||||
width: 100%; |
|
||||
max-width: 400px; |
|
||||
height: 300px; |
|
||||
margin: 20px auto; |
|
||||
border: 2px solid #ddd; |
|
||||
border-radius: 8px; |
|
||||
overflow: hidden; |
|
||||
} |
|
||||
|
|
||||
.camera-video { |
|
||||
width: 100%; |
|
||||
height: 100%; |
|
||||
object-fit: cover; |
|
||||
} |
|
||||
|
|
||||
.scanning-overlay { |
|
||||
position: absolute; |
|
||||
top: 0; |
|
||||
left: 0; |
|
||||
right: 0; |
|
||||
bottom: 0; |
|
||||
display: flex; |
|
||||
align-items: center; |
|
||||
justify-content: center; |
|
||||
} |
|
||||
|
|
||||
.scan-frame { |
|
||||
width: 200px; |
|
||||
height: 200px; |
|
||||
border: 2px solid #00ff00; |
|
||||
border-radius: 8px; |
|
||||
position: relative; |
|
||||
} |
|
||||
|
|
||||
.scan-line { |
|
||||
position: absolute; |
|
||||
top: 0; |
|
||||
left: 0; |
|
||||
right: 0; |
|
||||
height: 2px; |
|
||||
background: #00ff00; |
|
||||
animation: scan 2s linear infinite; |
|
||||
} |
|
||||
|
|
||||
@keyframes scan { |
|
||||
0% { |
|
||||
top: 0; |
|
||||
} |
|
||||
100% { |
|
||||
top: 100%; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
.scan-results { |
|
||||
margin-top: 20px; |
|
||||
} |
|
||||
|
|
||||
.results-list { |
|
||||
max-height: 300px; |
|
||||
overflow-y: auto; |
|
||||
border: 1px solid #ddd; |
|
||||
border-radius: 4px; |
|
||||
background: #fff; |
|
||||
margin-bottom: 15px; |
|
||||
} |
|
||||
|
|
||||
.result-item { |
|
||||
padding: 12px; |
|
||||
border-bottom: 1px solid #eee; |
|
||||
} |
|
||||
|
|
||||
.result-item:last-child { |
|
||||
border-bottom: none; |
|
||||
} |
|
||||
|
|
||||
.result-header { |
|
||||
display: flex; |
|
||||
justify-content: space-between; |
|
||||
margin-bottom: 8px; |
|
||||
} |
|
||||
|
|
||||
.result-number { |
|
||||
font-weight: bold; |
|
||||
color: #007bff; |
|
||||
} |
|
||||
|
|
||||
.result-time { |
|
||||
font-size: 0.85em; |
|
||||
color: #666; |
|
||||
} |
|
||||
|
|
||||
.result-content { |
|
||||
margin-bottom: 10px; |
|
||||
} |
|
||||
|
|
||||
.qr-data, |
|
||||
.qr-format { |
|
||||
margin-bottom: 5px; |
|
||||
word-break: break-all; |
|
||||
} |
|
||||
|
|
||||
.result-actions { |
|
||||
display: flex; |
|
||||
gap: 8px; |
|
||||
} |
|
||||
|
|
||||
.copy-btn, |
|
||||
.remove-btn { |
|
||||
padding: 4px 8px; |
|
||||
border: 1px solid #ccc; |
|
||||
border-radius: 3px; |
|
||||
background: #fff; |
|
||||
cursor: pointer; |
|
||||
font-size: 0.8em; |
|
||||
} |
|
||||
|
|
||||
.copy-btn:hover { |
|
||||
background: #e9ecef; |
|
||||
} |
|
||||
|
|
||||
.remove-btn:hover { |
|
||||
background: #f8d7da; |
|
||||
color: #721c24; |
|
||||
} |
|
||||
|
|
||||
.results-actions { |
|
||||
display: flex; |
|
||||
gap: 10px; |
|
||||
} |
|
||||
|
|
||||
.clear-btn, |
|
||||
.export-btn { |
|
||||
padding: 8px 16px; |
|
||||
border: 1px solid #ccc; |
|
||||
border-radius: 4px; |
|
||||
background: #fff; |
|
||||
cursor: pointer; |
|
||||
} |
|
||||
|
|
||||
.clear-btn:hover { |
|
||||
background: #f8d7da; |
|
||||
color: #721c24; |
|
||||
} |
|
||||
|
|
||||
.export-btn:hover { |
|
||||
background: #d4edda; |
|
||||
color: #155724; |
|
||||
} |
|
||||
|
|
||||
.settings-panel { |
|
||||
margin-top: 20px; |
|
||||
padding: 15px; |
|
||||
background: #f8f9fa; |
|
||||
border-radius: 4px; |
|
||||
} |
|
||||
|
|
||||
.setting-group { |
|
||||
margin-bottom: 15px; |
|
||||
} |
|
||||
|
|
||||
.setting-group label { |
|
||||
display: flex; |
|
||||
align-items: center; |
|
||||
gap: 8px; |
|
||||
font-weight: bold; |
|
||||
margin-bottom: 5px; |
|
||||
} |
|
||||
|
|
||||
.setting-group input[type="number"] { |
|
||||
width: 100px; |
|
||||
padding: 4px 8px; |
|
||||
border: 1px solid #ccc; |
|
||||
border-radius: 3px; |
|
||||
} |
|
||||
|
|
||||
.setting-description { |
|
||||
font-size: 0.85em; |
|
||||
color: #666; |
|
||||
margin-top: 3px; |
|
||||
margin-left: 24px; |
|
||||
} |
|
||||
</style> |
|
@ -1,667 +0,0 @@ |
|||||
<template> |
|
||||
<div class="threejs-viewer"> |
|
||||
<h2>3D Model Viewer</h2> |
|
||||
|
|
||||
<!-- Viewer controls --> |
|
||||
<div class="viewer-controls"> |
|
||||
<button :disabled="isLoading || !modelUrl" @click="loadModel"> |
|
||||
{{ isLoading ? "Loading..." : "Load Model" }} |
|
||||
</button> |
|
||||
<button :disabled="!isModelLoaded" @click="resetCamera"> |
|
||||
Reset Camera |
|
||||
</button> |
|
||||
<button :disabled="!isModelLoaded" @click="toggleAnimation"> |
|
||||
{{ isAnimating ? "Stop" : "Start" }} Animation |
|
||||
</button> |
|
||||
<button :disabled="!isModelLoaded" @click="toggleWireframe"> |
|
||||
{{ showWireframe ? "Hide" : "Show" }} Wireframe |
|
||||
</button> |
|
||||
</div> |
|
||||
|
|
||||
<!-- Loading status --> |
|
||||
<div v-if="isLoading" class="loading-status"> |
|
||||
<div class="loading-spinner"></div> |
|
||||
<p>Loading 3D model...</p> |
|
||||
<p class="loading-detail">{{ loadingProgress }}% complete</p> |
|
||||
</div> |
|
||||
|
|
||||
<!-- Error status --> |
|
||||
<div v-if="loadError" class="error-status"> |
|
||||
<p>Failed to load model: {{ loadError }}</p> |
|
||||
<button class="retry-btn" @click="retryLoad">Retry</button> |
|
||||
</div> |
|
||||
|
|
||||
<!-- 3D Canvas --> |
|
||||
<div |
|
||||
ref="canvasContainer" |
|
||||
class="canvas-container" |
|
||||
:class="{ 'model-loaded': isModelLoaded }" |
|
||||
> |
|
||||
<canvas ref="threeCanvas" class="three-canvas"></canvas> |
|
||||
|
|
||||
<!-- Overlay controls --> |
|
||||
<div v-if="isModelLoaded" class="overlay-controls"> |
|
||||
<div class="control-group"> |
|
||||
<label>Camera Distance:</label> |
|
||||
<input |
|
||||
v-model.number="cameraDistance" |
|
||||
type="range" |
|
||||
min="1" |
|
||||
max="20" |
|
||||
step="0.1" |
|
||||
@input="updateCameraDistance" |
|
||||
/> |
|
||||
<span>{{ cameraDistance.toFixed(1) }}</span> |
|
||||
</div> |
|
||||
|
|
||||
<div class="control-group"> |
|
||||
<label>Rotation Speed:</label> |
|
||||
<input |
|
||||
v-model.number="rotationSpeed" |
|
||||
type="range" |
|
||||
min="0" |
|
||||
max="2" |
|
||||
step="0.1" |
|
||||
/> |
|
||||
<span>{{ rotationSpeed.toFixed(1) }}</span> |
|
||||
</div> |
|
||||
|
|
||||
<div class="control-group"> |
|
||||
<label>Light Intensity:</label> |
|
||||
<input |
|
||||
v-model.number="lightIntensity" |
|
||||
type="range" |
|
||||
min="0" |
|
||||
max="2" |
|
||||
step="0.1" |
|
||||
@input="updateLightIntensity" |
|
||||
/> |
|
||||
<span>{{ lightIntensity.toFixed(1) }}</span> |
|
||||
</div> |
|
||||
</div> |
|
||||
|
|
||||
<!-- Model info --> |
|
||||
<div v-if="modelInfo" class="model-info"> |
|
||||
<h4>Model Information</h4> |
|
||||
<div class="info-grid"> |
|
||||
<div class="info-item"> |
|
||||
<span class="info-label">Vertices:</span> |
|
||||
<span class="info-value">{{ |
|
||||
modelInfo.vertexCount.toLocaleString() |
|
||||
}}</span> |
|
||||
</div> |
|
||||
<div class="info-item"> |
|
||||
<span class="info-label">Faces:</span> |
|
||||
<span class="info-value">{{ |
|
||||
modelInfo.faceCount.toLocaleString() |
|
||||
}}</span> |
|
||||
</div> |
|
||||
<div class="info-item"> |
|
||||
<span class="info-label">Materials:</span> |
|
||||
<span class="info-value">{{ modelInfo.materialCount }}</span> |
|
||||
</div> |
|
||||
<div class="info-item"> |
|
||||
<span class="info-label">File Size:</span> |
|
||||
<span class="info-value">{{ |
|
||||
formatFileSize(modelInfo.fileSize) |
|
||||
}}</span> |
|
||||
</div> |
|
||||
</div> |
|
||||
</div> |
|
||||
</div> |
|
||||
|
|
||||
<!-- Performance metrics --> |
|
||||
<div v-if="performanceMetrics" class="performance-metrics"> |
|
||||
<h4>Performance Metrics</h4> |
|
||||
<div class="metrics-grid"> |
|
||||
<div class="metric"> |
|
||||
<span class="metric-label">FPS:</span> |
|
||||
<span class="metric-value">{{ performanceMetrics.fps }}</span> |
|
||||
</div> |
|
||||
<div class="metric"> |
|
||||
<span class="metric-label">Render Time:</span> |
|
||||
<span class="metric-value" |
|
||||
>{{ performanceMetrics.renderTime }}ms</span |
|
||||
> |
|
||||
</div> |
|
||||
<div class="metric"> |
|
||||
<span class="metric-label">Memory Usage:</span> |
|
||||
<span class="metric-value" |
|
||||
>{{ performanceMetrics.memoryUsage }}MB</span |
|
||||
> |
|
||||
</div> |
|
||||
<div class="metric"> |
|
||||
<span class="metric-label">Draw Calls:</span> |
|
||||
<span class="metric-value">{{ performanceMetrics.drawCalls }}</span> |
|
||||
</div> |
|
||||
</div> |
|
||||
</div> |
|
||||
</div> |
|
||||
</template> |
|
||||
|
|
||||
<script lang="ts"> |
|
||||
import { Component, Vue, Prop, Emit } from "vue-facing-decorator"; |
|
||||
|
|
||||
interface ModelInfo { |
|
||||
vertexCount: number; |
|
||||
faceCount: number; |
|
||||
materialCount: number; |
|
||||
fileSize: number; |
|
||||
boundingBox: { |
|
||||
min: { x: number; y: number; z: number }; |
|
||||
max: { x: number; y: number; z: number }; |
|
||||
}; |
|
||||
} |
|
||||
|
|
||||
interface PerformanceMetrics { |
|
||||
fps: number; |
|
||||
renderTime: number; |
|
||||
memoryUsage: number; |
|
||||
drawCalls: number; |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* ThreeJS 3D Model Viewer Component |
|
||||
* |
|
||||
* Demonstrates lazy loading for heavy 3D rendering libraries. |
|
||||
* This component would benefit from lazy loading as ThreeJS is a large |
|
||||
* library that's only needed for 3D visualization features. |
|
||||
* |
|
||||
* @author Matthew Raymer |
|
||||
* @version 1.0.0 |
|
||||
*/ |
|
||||
@Component({ |
|
||||
name: "ThreeJSViewer", |
|
||||
}) |
|
||||
export default class ThreeJSViewer extends Vue { |
|
||||
@Prop({ required: true }) readonly modelUrl!: string; |
|
||||
|
|
||||
// Component state |
|
||||
isLoading = false; |
|
||||
isModelLoaded = false; |
|
||||
loadError: string | null = null; |
|
||||
loadingProgress = 0; |
|
||||
|
|
||||
// Animation state |
|
||||
isAnimating = false; |
|
||||
showWireframe = false; |
|
||||
|
|
||||
// Camera and lighting controls |
|
||||
cameraDistance = 5; |
|
||||
rotationSpeed = 0.5; |
|
||||
lightIntensity = 1; |
|
||||
|
|
||||
// Canvas references |
|
||||
canvasContainer: HTMLElement | null = null; |
|
||||
threeCanvas: HTMLCanvasElement | null = null; |
|
||||
|
|
||||
// Model and performance data |
|
||||
modelInfo: ModelInfo | null = null; |
|
||||
performanceMetrics: PerformanceMetrics | null = null; |
|
||||
|
|
||||
// ThreeJS objects (will be lazy loaded) |
|
||||
private three: any = null; |
|
||||
private scene: any = null; |
|
||||
private camera: any = null; |
|
||||
private renderer: any = null; |
|
||||
private model: any = null; |
|
||||
private controls: any = null; |
|
||||
private animationId: number | null = null; |
|
||||
private frameCount = 0; |
|
||||
private lastTime = 0; |
|
||||
|
|
||||
// Lifecycle hooks |
|
||||
mounted(): void { |
|
||||
console.log("[ThreeJSViewer] Component mounted"); |
|
||||
this.initializeCanvas(); |
|
||||
} |
|
||||
|
|
||||
beforeUnmount(): void { |
|
||||
this.cleanup(); |
|
||||
console.log("[ThreeJSViewer] Component unmounting"); |
|
||||
} |
|
||||
|
|
||||
// Methods |
|
||||
private initializeCanvas(): void { |
|
||||
this.canvasContainer = this.$refs.canvasContainer as HTMLElement; |
|
||||
this.threeCanvas = this.$refs.threeCanvas as HTMLCanvasElement; |
|
||||
|
|
||||
if (this.threeCanvas) { |
|
||||
this.threeCanvas.width = this.canvasContainer.clientWidth; |
|
||||
this.threeCanvas.height = this.canvasContainer.clientHeight; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
async loadModel(): Promise<void> { |
|
||||
if (this.isLoading || !this.modelUrl) return; |
|
||||
|
|
||||
this.isLoading = true; |
|
||||
this.loadError = null; |
|
||||
this.loadingProgress = 0; |
|
||||
|
|
||||
try { |
|
||||
console.log("[ThreeJSViewer] Loading 3D model:", this.modelUrl); |
|
||||
|
|
||||
// Lazy load ThreeJS |
|
||||
await this.loadThreeJS(); |
|
||||
|
|
||||
// Initialize scene |
|
||||
await this.initializeScene(); |
|
||||
|
|
||||
// Load model |
|
||||
await this.loadModelFile(); |
|
||||
|
|
||||
// Start rendering |
|
||||
this.startRendering(); |
|
||||
|
|
||||
this.isModelLoaded = true; |
|
||||
this.isLoading = false; |
|
||||
|
|
||||
// Emit model loaded event |
|
||||
this.$emit("model-loaded", this.modelInfo); |
|
||||
|
|
||||
console.log("[ThreeJSViewer] Model loaded successfully"); |
|
||||
} catch (error) { |
|
||||
console.error("[ThreeJSViewer] Failed to load model:", error); |
|
||||
this.loadError = error instanceof Error ? error.message : "Unknown error"; |
|
||||
this.isLoading = false; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
private async loadThreeJS(): Promise<void> { |
|
||||
// Simulate loading ThreeJS library |
|
||||
this.loadingProgress = 20; |
|
||||
await this.simulateLoading(500); |
|
||||
|
|
||||
// In a real implementation, you would import ThreeJS here |
|
||||
// this.three = await import('three'); |
|
||||
|
|
||||
this.loadingProgress = 40; |
|
||||
await this.simulateLoading(300); |
|
||||
} |
|
||||
|
|
||||
private async initializeScene(): Promise<void> { |
|
||||
this.loadingProgress = 60; |
|
||||
|
|
||||
// Simulate scene initialization |
|
||||
await this.simulateLoading(400); |
|
||||
|
|
||||
// In a real implementation, you would set up ThreeJS scene here |
|
||||
// this.scene = new this.three.Scene(); |
|
||||
// this.camera = new this.three.PerspectiveCamera(75, width / height, 0.1, 1000); |
|
||||
// this.renderer = new this.three.WebGLRenderer({ canvas: this.threeCanvas }); |
|
||||
|
|
||||
this.loadingProgress = 80; |
|
||||
} |
|
||||
|
|
||||
private async loadModelFile(): Promise<void> { |
|
||||
this.loadingProgress = 90; |
|
||||
|
|
||||
// Simulate model loading |
|
||||
await this.simulateLoading(600); |
|
||||
|
|
||||
// Simulate model info |
|
||||
this.modelInfo = { |
|
||||
vertexCount: Math.floor(Math.random() * 50000) + 1000, |
|
||||
faceCount: Math.floor(Math.random() * 25000) + 500, |
|
||||
materialCount: Math.floor(Math.random() * 5) + 1, |
|
||||
fileSize: Math.floor(Math.random() * 5000000) + 100000, |
|
||||
boundingBox: { |
|
||||
min: { x: -1, y: -1, z: -1 }, |
|
||||
max: { x: 1, y: 1, z: 1 }, |
|
||||
}, |
|
||||
}; |
|
||||
|
|
||||
this.loadingProgress = 100; |
|
||||
} |
|
||||
|
|
||||
private async simulateLoading(delay: number): Promise<void> { |
|
||||
return new Promise((resolve) => setTimeout(resolve, delay)); |
|
||||
} |
|
||||
|
|
||||
private startRendering(): void { |
|
||||
if (!this.isModelLoaded) return; |
|
||||
|
|
||||
this.isAnimating = true; |
|
||||
this.animate(); |
|
||||
|
|
||||
// Start performance monitoring |
|
||||
this.startPerformanceMonitoring(); |
|
||||
} |
|
||||
|
|
||||
private animate(): void { |
|
||||
if (!this.isAnimating) return; |
|
||||
|
|
||||
this.animationId = requestAnimationFrame(() => this.animate()); |
|
||||
|
|
||||
// Simulate model rotation |
|
||||
if (this.model && this.rotationSpeed > 0) { |
|
||||
// this.model.rotation.y += this.rotationSpeed * 0.01; |
|
||||
} |
|
||||
|
|
||||
// Simulate rendering |
|
||||
// this.renderer.render(this.scene, this.camera); |
|
||||
|
|
||||
this.frameCount++; |
|
||||
} |
|
||||
|
|
||||
private startPerformanceMonitoring(): void { |
|
||||
const updateMetrics = () => { |
|
||||
if (!this.isAnimating) return; |
|
||||
|
|
||||
const now = performance.now(); |
|
||||
const deltaTime = now - this.lastTime; |
|
||||
|
|
||||
if (deltaTime > 0) { |
|
||||
const fps = Math.round(1000 / deltaTime); |
|
||||
|
|
||||
this.performanceMetrics = { |
|
||||
fps: Math.min(fps, 60), // Cap at 60 FPS for display |
|
||||
renderTime: Math.round(deltaTime), |
|
||||
memoryUsage: Math.round((Math.random() * 50 + 10) * 100) / 100, |
|
||||
drawCalls: Math.floor(Math.random() * 100) + 10, |
|
||||
}; |
|
||||
} |
|
||||
|
|
||||
this.lastTime = now; |
|
||||
requestAnimationFrame(updateMetrics); |
|
||||
}; |
|
||||
|
|
||||
updateMetrics(); |
|
||||
} |
|
||||
|
|
||||
resetCamera(): void { |
|
||||
if (!this.isModelLoaded) return; |
|
||||
|
|
||||
this.cameraDistance = 5; |
|
||||
this.updateCameraDistance(); |
|
||||
console.log("[ThreeJSViewer] Camera reset"); |
|
||||
} |
|
||||
|
|
||||
toggleAnimation(): void { |
|
||||
this.isAnimating = !this.isAnimating; |
|
||||
|
|
||||
if (this.isAnimating) { |
|
||||
this.animate(); |
|
||||
} else if (this.animationId) { |
|
||||
cancelAnimationFrame(this.animationId); |
|
||||
this.animationId = null; |
|
||||
} |
|
||||
|
|
||||
console.log("[ThreeJSViewer] Animation toggled:", this.isAnimating); |
|
||||
} |
|
||||
|
|
||||
toggleWireframe(): void { |
|
||||
this.showWireframe = !this.showWireframe; |
|
||||
|
|
||||
// In a real implementation, you would toggle wireframe mode |
|
||||
// this.model.traverse((child: any) => { |
|
||||
// if (child.isMesh) { |
|
||||
// child.material.wireframe = this.showWireframe; |
|
||||
// } |
|
||||
// }); |
|
||||
|
|
||||
console.log("[ThreeJSViewer] Wireframe toggled:", this.showWireframe); |
|
||||
} |
|
||||
|
|
||||
updateCameraDistance(): void { |
|
||||
if (!this.isModelLoaded) return; |
|
||||
|
|
||||
// In a real implementation, you would update camera position |
|
||||
// this.camera.position.z = this.cameraDistance; |
|
||||
// this.camera.lookAt(0, 0, 0); |
|
||||
} |
|
||||
|
|
||||
updateLightIntensity(): void { |
|
||||
if (!this.isModelLoaded) return; |
|
||||
|
|
||||
// In a real implementation, you would update light intensity |
|
||||
// this.light.intensity = this.lightIntensity; |
|
||||
} |
|
||||
|
|
||||
retryLoad(): void { |
|
||||
this.loadError = null; |
|
||||
this.loadModel(); |
|
||||
} |
|
||||
|
|
||||
private cleanup(): void { |
|
||||
if (this.animationId) { |
|
||||
cancelAnimationFrame(this.animationId); |
|
||||
this.animationId = null; |
|
||||
} |
|
||||
|
|
||||
if (this.renderer) { |
|
||||
this.renderer.dispose(); |
|
||||
} |
|
||||
|
|
||||
this.isAnimating = false; |
|
||||
this.isModelLoaded = false; |
|
||||
} |
|
||||
|
|
||||
formatFileSize(bytes: number): string { |
|
||||
const sizes = ["B", "KB", "MB", "GB"]; |
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(1024)); |
|
||||
return `${(bytes / Math.pow(1024, i)).toFixed(1)} ${sizes[i]}`; |
|
||||
} |
|
||||
|
|
||||
// Event emitters |
|
||||
@Emit("model-loaded") |
|
||||
emitModelLoaded(info: ModelInfo): ModelInfo { |
|
||||
return info; |
|
||||
} |
|
||||
} |
|
||||
</script> |
|
||||
|
|
||||
<style scoped> |
|
||||
.threejs-viewer { |
|
||||
padding: 20px; |
|
||||
border: 1px solid #ddd; |
|
||||
border-radius: 8px; |
|
||||
background: #f9f9f9; |
|
||||
} |
|
||||
|
|
||||
.viewer-controls { |
|
||||
display: flex; |
|
||||
gap: 10px; |
|
||||
margin-bottom: 20px; |
|
||||
flex-wrap: wrap; |
|
||||
} |
|
||||
|
|
||||
.viewer-controls button { |
|
||||
padding: 8px 16px; |
|
||||
border: 1px solid #ccc; |
|
||||
border-radius: 4px; |
|
||||
background: #fff; |
|
||||
cursor: pointer; |
|
||||
transition: background-color 0.2s; |
|
||||
} |
|
||||
|
|
||||
.viewer-controls button:hover:not(:disabled) { |
|
||||
background: #e9ecef; |
|
||||
} |
|
||||
|
|
||||
.viewer-controls button:disabled { |
|
||||
opacity: 0.6; |
|
||||
cursor: not-allowed; |
|
||||
} |
|
||||
|
|
||||
.loading-status { |
|
||||
display: flex; |
|
||||
flex-direction: column; |
|
||||
align-items: center; |
|
||||
justify-content: center; |
|
||||
padding: 40px; |
|
||||
text-align: center; |
|
||||
} |
|
||||
|
|
||||
.loading-spinner { |
|
||||
width: 40px; |
|
||||
height: 40px; |
|
||||
border: 4px solid #f3f3f3; |
|
||||
border-top: 4px solid #3498db; |
|
||||
border-radius: 50%; |
|
||||
animation: spin 1s linear infinite; |
|
||||
margin-bottom: 20px; |
|
||||
} |
|
||||
|
|
||||
@keyframes spin { |
|
||||
0% { |
|
||||
transform: rotate(0deg); |
|
||||
} |
|
||||
100% { |
|
||||
transform: rotate(360deg); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
.loading-detail { |
|
||||
font-size: 0.9em; |
|
||||
color: #666; |
|
||||
margin-top: 10px; |
|
||||
} |
|
||||
|
|
||||
.error-status { |
|
||||
padding: 20px; |
|
||||
background: #f8d7da; |
|
||||
border: 1px solid #f5c6cb; |
|
||||
border-radius: 4px; |
|
||||
color: #721c24; |
|
||||
text-align: center; |
|
||||
} |
|
||||
|
|
||||
.retry-btn { |
|
||||
margin-top: 10px; |
|
||||
padding: 8px 16px; |
|
||||
border: 1px solid #721c24; |
|
||||
border-radius: 4px; |
|
||||
background: #721c24; |
|
||||
color: #fff; |
|
||||
cursor: pointer; |
|
||||
} |
|
||||
|
|
||||
.retry-btn:hover { |
|
||||
background: #5a1a1a; |
|
||||
} |
|
||||
|
|
||||
.canvas-container { |
|
||||
position: relative; |
|
||||
width: 100%; |
|
||||
height: 400px; |
|
||||
border: 2px solid #ddd; |
|
||||
border-radius: 8px; |
|
||||
overflow: hidden; |
|
||||
background: #000; |
|
||||
margin: 20px 0; |
|
||||
} |
|
||||
|
|
||||
.canvas-container.model-loaded { |
|
||||
border-color: #28a745; |
|
||||
} |
|
||||
|
|
||||
.three-canvas { |
|
||||
width: 100%; |
|
||||
height: 100%; |
|
||||
display: block; |
|
||||
} |
|
||||
|
|
||||
.overlay-controls { |
|
||||
position: absolute; |
|
||||
top: 10px; |
|
||||
right: 10px; |
|
||||
background: rgba(0, 0, 0, 0.8); |
|
||||
color: #fff; |
|
||||
padding: 15px; |
|
||||
border-radius: 4px; |
|
||||
min-width: 200px; |
|
||||
} |
|
||||
|
|
||||
.control-group { |
|
||||
margin-bottom: 10px; |
|
||||
} |
|
||||
|
|
||||
.control-group:last-child { |
|
||||
margin-bottom: 0; |
|
||||
} |
|
||||
|
|
||||
.control-group label { |
|
||||
display: block; |
|
||||
font-size: 0.9em; |
|
||||
margin-bottom: 5px; |
|
||||
} |
|
||||
|
|
||||
.control-group input[type="range"] { |
|
||||
width: 100%; |
|
||||
margin-right: 10px; |
|
||||
} |
|
||||
|
|
||||
.control-group span { |
|
||||
font-size: 0.8em; |
|
||||
color: #ccc; |
|
||||
} |
|
||||
|
|
||||
.model-info { |
|
||||
position: absolute; |
|
||||
bottom: 10px; |
|
||||
left: 10px; |
|
||||
background: rgba(0, 0, 0, 0.8); |
|
||||
color: #fff; |
|
||||
padding: 15px; |
|
||||
border-radius: 4px; |
|
||||
min-width: 200px; |
|
||||
} |
|
||||
|
|
||||
.model-info h4 { |
|
||||
margin: 0 0 10px 0; |
|
||||
font-size: 1em; |
|
||||
} |
|
||||
|
|
||||
.info-grid { |
|
||||
display: grid; |
|
||||
gap: 5px; |
|
||||
} |
|
||||
|
|
||||
.info-item { |
|
||||
display: flex; |
|
||||
justify-content: space-between; |
|
||||
font-size: 0.85em; |
|
||||
} |
|
||||
|
|
||||
.info-label { |
|
||||
color: #ccc; |
|
||||
} |
|
||||
|
|
||||
.info-value { |
|
||||
font-weight: bold; |
|
||||
} |
|
||||
|
|
||||
.performance-metrics { |
|
||||
margin-top: 20px; |
|
||||
padding: 15px; |
|
||||
background: #e8f4fd; |
|
||||
border-radius: 4px; |
|
||||
} |
|
||||
|
|
||||
.metrics-grid { |
|
||||
display: grid; |
|
||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); |
|
||||
gap: 15px; |
|
||||
margin-top: 10px; |
|
||||
} |
|
||||
|
|
||||
.metric { |
|
||||
display: flex; |
|
||||
justify-content: space-between; |
|
||||
padding: 8px; |
|
||||
background: #fff; |
|
||||
border-radius: 4px; |
|
||||
} |
|
||||
|
|
||||
.metric-label { |
|
||||
font-weight: bold; |
|
||||
color: #333; |
|
||||
} |
|
||||
|
|
||||
.metric-value { |
|
||||
color: #007bff; |
|
||||
font-weight: bold; |
|
||||
} |
|
||||
</style> |
|
@ -1,70 +0,0 @@ |
|||||
import { defineConfig } from "vite"; |
|
||||
import vue from "@vitejs/plugin-vue"; |
|
||||
import path from "path"; |
|
||||
import { fileURLToPath } from 'url'; |
|
||||
|
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url)); |
|
||||
|
|
||||
export default defineConfig({ |
|
||||
plugins: [vue()], |
|
||||
resolve: { |
|
||||
alias: { |
|
||||
'@': path.resolve(__dirname, 'src'), |
|
||||
'@nostr/tools': path.resolve(__dirname, 'node_modules/@nostr/tools'), |
|
||||
'@nostr/tools/nip06': path.resolve(__dirname, 'node_modules/@nostr/tools/nip06'), |
|
||||
stream: 'stream-browserify', |
|
||||
util: 'util', |
|
||||
crypto: 'crypto-browserify', |
|
||||
assert: 'assert/', |
|
||||
http: 'stream-http', |
|
||||
https: 'https-browserify', |
|
||||
url: 'url/', |
|
||||
zlib: 'browserify-zlib', |
|
||||
path: 'path-browserify', |
|
||||
fs: false, |
|
||||
tty: 'tty-browserify', |
|
||||
net: false, |
|
||||
dns: false, |
|
||||
child_process: false, |
|
||||
os: false |
|
||||
}, |
|
||||
mainFields: ['module', 'jsnext:main', 'jsnext', 'main'], |
|
||||
}, |
|
||||
optimizeDeps: { |
|
||||
include: ['@nostr/tools', '@nostr/tools/nip06', '@jlongster/sql.js'], |
|
||||
esbuildOptions: { |
|
||||
define: { |
|
||||
global: 'globalThis' |
|
||||
} |
|
||||
} |
|
||||
}, |
|
||||
build: { |
|
||||
sourcemap: true, |
|
||||
target: 'esnext', |
|
||||
chunkSizeWarningLimit: 1000, |
|
||||
commonjsOptions: { |
|
||||
include: [/node_modules/], |
|
||||
transformMixedEsModules: true |
|
||||
}, |
|
||||
rollupOptions: { |
|
||||
external: [ |
|
||||
'stream', 'util', 'crypto', 'http', 'https', 'url', 'zlib', |
|
||||
'path', 'fs', 'tty', 'assert', 'net', 'dns', 'child_process', 'os' |
|
||||
], |
|
||||
output: { |
|
||||
globals: { |
|
||||
stream: 'stream', |
|
||||
util: 'util', |
|
||||
crypto: 'crypto', |
|
||||
http: 'http', |
|
||||
https: 'https', |
|
||||
url: 'url', |
|
||||
zlib: 'zlib', |
|
||||
path: 'path', |
|
||||
assert: 'assert', |
|
||||
tty: 'tty' |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
}); |
|
@ -1,47 +0,0 @@ |
|||||
import { defineConfig } from "vite"; |
|
||||
import vue from "@vitejs/plugin-vue"; |
|
||||
import path from "path"; |
|
||||
|
|
||||
export default defineConfig({ |
|
||||
plugins: [vue()], |
|
||||
// CORS headers removed to allow images from any domain
|
|
||||
resolve: { |
|
||||
alias: { |
|
||||
'@': path.resolve(__dirname, 'src'), |
|
||||
'@nostr/tools': path.resolve(__dirname, 'node_modules/@nostr/tools'), |
|
||||
'@nostr/tools/nip06': path.resolve(__dirname, 'node_modules/@nostr/tools/nip06'), |
|
||||
stream: 'stream-browserify', |
|
||||
util: 'util', |
|
||||
crypto: 'crypto-browserify' |
|
||||
}, |
|
||||
mainFields: ['module', 'jsnext:main', 'jsnext', 'main'], |
|
||||
}, |
|
||||
optimizeDeps: { |
|
||||
include: ['@nostr/tools', '@nostr/tools/nip06', '@jlongster/sql.js'], |
|
||||
esbuildOptions: { |
|
||||
define: { |
|
||||
global: 'globalThis' |
|
||||
} |
|
||||
} |
|
||||
}, |
|
||||
build: { |
|
||||
sourcemap: true, |
|
||||
target: 'esnext', |
|
||||
chunkSizeWarningLimit: 1000, |
|
||||
commonjsOptions: { |
|
||||
include: [/node_modules/], |
|
||||
transformMixedEsModules: true |
|
||||
}, |
|
||||
rollupOptions: { |
|
||||
external: ['stream', 'util', 'crypto'], |
|
||||
output: { |
|
||||
globals: { |
|
||||
stream: 'stream', |
|
||||
util: 'util', |
|
||||
crypto: 'crypto' |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
}, |
|
||||
assetsInclude: ['**/*.wasm'] |
|
||||
}); |
|
Loading…
Reference in new issue