JavaScript类/原型重构分离关注点及Three.js文件逻辑拆分方案
Hey there! Let's break down your two questions with practical, clean code examples—separating concerns is such a key part of maintainable JavaScript, so great call to refactor!
The core idea is to split your code into distinct units (classes/prototypes) where each handles one specific job, and use this to access the instance's own methods and properties. Let's walk through both ES6 classes (the modern approach) and prototype-based code (the underlying mechanism).
ES6 Class Approach
Let's say you had tangled code mixing data management and UI rendering. Here's how to split that into two focused classes:
// Handles user data logic only class UserModel { constructor(name, age) { this.name = name; this.age = age; } // Instance method: Update age with validation updateAge(newAge) { if (typeof newAge === 'number' && newAge > 0) { this.age = newAge; return true; } console.error("Invalid age value"); return false; } // Instance method: Get structured user data getUserData() { return { name: this.name, age: this.age }; } } // Handles UI rendering logic only class UserView { constructor(userModel) { this.userModel = userModel; // Inject the dependency instead of hardcoding } // Call the model's method to get data, then render renderUserInfo() { const { name, age } = this.userModel.getUserData(); console.log(`User: ${name}, Age: ${age}`); } } // Usage const alice = new UserModel("Alice", 30); const userView = new UserView(alice); userView.renderUserInfo(); // Output: User: Alice, Age: 30 alice.updateAge(31); userView.renderUserInfo(); // Output: User: Alice, Age: 31
Prototype-Based Approach
ES6 classes are syntactic sugar over JavaScript's prototype system. If you need to work with older environments or want to understand the low-level implementation:
// UserModel constructor function UserModel(name, age) { this.name = name; this.age = age; } // Add methods to the prototype UserModel.prototype.updateAge = function(newAge) { if (typeof newAge === 'number' && newAge > 0) { this.age = newAge; return true; } console.error("Invalid age value"); return false; }; UserModel.prototype.getUserData = function() { return { name: this.name, age: this.age }; }; // UserView constructor function UserView(userModel) { this.userModel = userModel; } UserView.prototype.renderUserInfo = function() { const { name, age } = this.userModel.getUserData(); console.log(`User: ${name}, Age: ${age}`); }; // Usage is identical to the class approach const bob = new UserModel("Bob", 25); const userView = new UserView(bob); userView.renderUserInfo(); bob.updateAge(26); userView.renderUserInfo();
Key Takeaways
- Single Responsibility: Each class/prototype does one job (data vs. UI) to keep code maintainable.
thisKeyword: Usethisinside instance methods to access the current object's properties and other methods.- Dependency Injection: Pass required objects (like the model into the view) instead of creating them inside the class—this reduces coupling and makes testing easier.
Since you already have InitCanvas.js handling rendering, we'll create a standalone FileHandler class that focuses only on file operations. This class won't know anything about your canvas instances—instead, it will return data that your main app can pass to InitCanvas.
Step 1: Implement the FileHandler Class
This class will handle local file reading, model parsing (using Three.js loaders), and model downloading. We'll use Promises to handle async operations cleanly.
// FileHandler.js class FileHandler { constructor() { // Initialize Three.js loaders you need this.gltfLoader = new THREE.GLTFLoader(); this.objLoader = new THREE.OBJLoader(); } // Read a local file from an input element loadLocalFile(fileInput) { return new Promise((resolve, reject) => { const file = fileInput.files[0]; if (!file) { reject(new Error("No file selected")); return; } const reader = new FileReader(); reader.onload = (e) => resolve({ file: file, content: e.target.result, fileName: file.name }); reader.onerror = () => reject(new Error("Failed to read file")); // Choose read method based on file type if (file.type.includes("text") || file.name.endsWith(".obj")) { reader.readAsText(file); } else { reader.readAsArrayBuffer(file); } }); } // Parse the file content into a Three.js mesh/scene parseModelFile(fileData) { return new Promise((resolve, reject) => { const { content, fileName } = fileData; if (fileName.endsWith(".gltf") || fileName.endsWith(".glb")) { this.gltfLoader.loadAsync(content) .then(gltf => resolve(gltf.scene)) .catch(err => reject(new Error(`GLTF parse error: ${err.message}`))); } else if (fileName.endsWith(".obj")) { try { const mesh = this.objLoader.parse(content); resolve(mesh); } catch (err) { reject(new Error(`OBJ parse error: ${err.message}`)); } } else { reject(new Error("Unsupported file type")); } }); } // Download a Three.js scene/mesh as a GLTF file downloadModel(sceneOrMesh, fileName = "exported-model.gltf") { const exporter = new THREE.GLTFExporter(); exporter.parse( sceneOrMesh, (gltf) => { const blob = new Blob([JSON.stringify(gltf)], { type: "application/json" }); const url = URL.createObjectURL(blob); const link = document.createElement("a"); link.href = url; link.download = fileName; link.click(); URL.revokeObjectURL(url); }, (err) => console.error("Export failed:", err) ); } } export default FileHandler;
Step 2: Integrate with InitCanvas.js
Your InitCanvas class needs a few methods to accept models and expose its scene. Here's how the main app ties everything together:
// app.js (your main entry point) import InitCanvas from './InitCanvas.js'; import FileHandler from './FileHandler.js'; // Initialize your canvas instances and file handler const canvas1 = new InitCanvas("#canvas-1"); const canvas2 = new InitCanvas("#canvas-2"); const fileHandler = new FileHandler(); // Handle file selection const fileInput = document.getElementById("model-input"); fileInput.addEventListener("change", async (e) => { try { // Read and parse the file const fileData = await fileHandler.loadLocalFile(e.target); const model = await fileHandler.parseModelFile(fileData); // Pass the model to each canvas (clone for the second canvas to avoid shared references) canvas1.addModel(model); canvas2.addModel(model.clone()); } catch (err) { console.error("File processing failed:", err); } }); // Handle download button click const downloadBtn = document.getElementById("download-btn"); downloadBtn.addEventListener("click", () => { // Export the scene from canvas 1 const scene = canvas1.getScene(); fileHandler.downloadModel(scene, "canvas1-model.gltf"); });
And update InitCanvas.js to include the required methods:
// InitCanvas.js class InitCanvas { constructor(canvasSelector) { this.canvas = document.querySelector(canvasSelector); this.scene = new THREE.Scene(); this.camera = new THREE.PerspectiveCamera(75, this.canvas.clientWidth / this.canvas.clientHeight, 0.1, 1000); this.renderer = new THREE.WebGLRenderer({ canvas: this.canvas }); this.initScene(); } initScene() { this.renderer.setSize(this.canvas.clientWidth, this.canvas.clientHeight); this.scene.add(new THREE.AmbientLight(0xffffff, 0.6)); this.scene.add(new THREE.DirectionalLight(0xffffff, 0.4)); this.animate(); } // Add a model to the scene and adjust camera to fit it addModel(model) { // Optional: Clear existing models (keep lights) this.scene.children = this.scene.children.filter(child => child.type.includes("Light")); this.scene.add(model); // Auto-fit camera to model const boundingBox = new THREE.Box3().setFromObject(model); const center = boundingBox.getCenter(new THREE.Vector3()); const size = boundingBox.getSize(new THREE.Vector3()); const maxDimension = Math.max(size.x, size.y, size.z); const cameraDistance = maxDimension / (2 * Math.tan(Math.PI * this.camera.fov / 360)); this.camera.position.set(center.x, center.y, cameraDistance); this.camera.lookAt(center); } // Expose the scene for downloading getScene() { return this.scene; } animate() { requestAnimationFrame(() => this.animate()); this.renderer.render(this.scene, this.camera); } } export default InitCanvas;
Why This Works
- Complete Separation:
FileHandlernever touches your canvas or rendering logic, andInitCanvasnever handles file operations. - Reusability: You can reuse
FileHandlerin other parts of your app that need file processing, andInitCanvascan be modified without breaking file logic. - Async Safety: Using Promises and
async/awaitensures file operations don't block the main thread and handle errors cleanly.
内容的提问来源于stack exchange,提问作者Yone

