You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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!

1. Refactoring JS Code with Classes/Prototypes to Separate Concerns & Call Instance Methods

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.
  • this Keyword: Use this inside 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.
2. Separating File Logic (Read/Parse/Download) from Three.js Canvas Rendering

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: FileHandler never touches your canvas or rendering logic, and InitCanvas never handles file operations.
  • Reusability: You can reuse FileHandler in other parts of your app that need file processing, and InitCanvas can be modified without breaking file logic.
  • Async Safety: Using Promises and async/await ensures file operations don't block the main thread and handle errors cleanly.

内容的提问来源于stack exchange,提问作者Yone

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.21 03:44:59