从零打造一个跨框架通用的 AI Agent 插件

📝 前言

在 AI 大模型时代,越来越多的 Web 应用需要集成智能对话功能。然而,前端技术栈的多样性(Vue、React、jQuery 等)给开发者带来了一个难题:如何开发一个一次编写、到处运行的 AI 对话插件?

本文记录了 AI Agent Plugin 项目从诞生到完成的全过程,分享技术选型、架构设计、核心实现以及遇到的挑战与解决方案。


🎯 项目背景与目标

痛点分析

在实际项目中,我们遇到以下问题:

  1. 技术栈碎片化:公司不同项目使用 Vue 2、Vue 3、React、jQuery 等不同技术栈
  2. 重复开发成本高:每个技术栈都要单独开发一套 AI 对话组件
  3. 维护困难:功能更新需要同步修改多个版本
  4. 样式冲突:插件样式容易被宿主项目覆盖

设计目标

基于以上痛点,我们确定了以下核心目标:

  • 跨框架兼容:一套代码支持 Vue、React、jQuery 等任意前端框架
  • 零依赖:不依赖任何第三方框架,纯原生实现
  • 样式隔离:避免与宿主项目样式冲突
  • TypeScript 支持:提供完整的类型定义
  • 主题定制:支持浅色/深色主题和自定义配色
  • 灵活配置:支持流式/普通响应、四角定位等

🏗️ 技术选型与架构设计

1. 模块化规范:UMD

为了实现跨框架兼容,我们选择了 UMD (Universal Module Definition) 规范:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// UMD 模式可以同时支持多种加载方式
// 1. AMD (RequireJS)
// 2. CommonJS (Node.js)
// 3. 全局变量 (浏览器直接引入)
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
define([], factory); // AMD
} else if (typeof module === 'object' && module.exports) {
module.exports = factory(); // CommonJS
} else {
root.AIAgent = factory(); // 全局变量
}
}(typeof self !== 'undefined' ? self : this, function () {
// 插件代码
}));

通过 Webpack 的 output.library 配置,我们可以自动生成 UMD 格式的代码:

1
2
3
4
5
6
7
8
9
10
11
// webpack.config.js
output: {
filename: 'ai-agent.js',
path: path.resolve(__dirname, 'dist'),
library: {
name: 'AIAgent',
type: 'umd',
export: 'default'
},
globalObject: 'this'
}

2. 开发语言:TypeScript

使用 TypeScript 开发带来的优势:

  1. 类型安全:编译时捕获错误,减少运行时 bug
  2. 智能提示:IDE 提供完整的代码提示和自动补全
  3. 可维护性:接口定义即文档,代码意图清晰

核心类型定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
export interface AIAgentOptions {
host?: string; // 后端域名
secret?: string; // API 密钥
stream?: boolean; // 流式响应
theme?: 'light' | 'dark'; // 主题
colors?: { // 自定义配色
primary?: string;
background?: string;
text?: string;
aiMessageBg?: string;
// ... 更多颜色配置
};
position?: 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left';
placeholder?: string;
title?: string;
}

3. 样式隔离:命名空间 + CSS 变量

为了避免样式冲突,我们采用了两种策略:

策略一:命名空间隔离

所有 CSS 类名添加 ai-agent- 前缀:

1
2
3
.ai-agent-panel { /* 面板 */ }
.ai-agent-btn { /* 按钮 */ }
.ai-agent-msg { /* 消息 */ }

策略二:CSS 变量动态主题

使用 CSS 变量实现主题定制:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
:root {
--ai-agent-primary: #4096ff;
--ai-agent-primary-hover: #2e80ff;
--ai-agent-bg: #ffffff;
--ai-agent-text: #333333;
/* ... 更多变量 */
}

/* 浅色主题 */
.ai-agent-theme-light {
--ai-agent-bg: #ffffff;
--ai-agent-text: #333333;
}

/* 深色主题 */
.ai-agent-theme-dark {
--ai-agent-bg: #1e1e1e;
--ai-agent-text: #f0f0f0;
}

通过 JavaScript 动态注入自定义颜色:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private injectStyles(): void {
const style = document.createElement('style');
let colorVars = '';

if (this.options.colors) {
if (this.options.colors.primary) {
colorVars += `--ai-agent-primary: ${this.options.colors.primary};\n`;
}
// ... 注入更多颜色变量
}

style.textContent = `
${colorVars ? `.ai-agent-theme-${this.options.theme} {\n${colorVars}}` : ''}
`;

document.head.appendChild(style);
}

🔧 核心功能实现

1. 插件生命周期管理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class AIAgent {
constructor(options: AIAgentOptions) {
// 1. 参数验证
this.validateOptions(options);

// 2. 配置合并
this.options = { /* 默认配置 + 用户配置 */ };

// 3. 计算端点 URL
this.endpoints = this.calculateEndpoints();

// 4. 初始化 UI
this.init();
}

private init(): void {
this.createTriggerButton(); // 创建触发按钮
this.createChatPanel(); // 创建对话面板
this.injectStyles(); // 注入样式
}

public destroy(): void {
// 清理 DOM 元素
if (this.buttonEl) this.buttonEl.remove();
if (this.panelEl) this.panelEl.remove();

// 清理事件监听器
// 清理内存引用
this.chatHistory = [];
}
}

2. 双模式 API 调用

插件支持普通模式流式模式两种 API 调用方式:

普通模式(一次性返回)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private async sendNormalMessage(text: string): Promise<void> {
const response = await fetch(this.endpoints.completion, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.options.secret}`
},
body: JSON.stringify({
content: text,
messages: this.chatHistory
})
});

const data: ApiResponse = await response.json();
this.appendMessage(data.data.content, 'ai');
}

流式模式(SSE 实时推送)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
private async sendStreamMessage(text: string): Promise<void> {
const response = await fetch(this.endpoints.stream, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.options.secret}`
},
body: JSON.stringify({
messages: this.chatHistory
})
});

const reader = response.body!.getReader();
const decoder = new TextDecoder();
let aiMessage = '';

const messageEl = this.createMessageElement('', 'ai');

while (true) {
const { done, value } = await reader.read();
if (done) break;

const chunk = decoder.decode(value);
const lines = chunk.split('\n');

for (const line of lines) {
if (line.startsWith('data: ')) {
const data = JSON.parse(line.slice(6));
aiMessage += data.content;
messageEl.textContent = aiMessage;
}
}
}
}

3. 智能端点路由

根据配置自动选择合适的 API 端点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
private calculateEndpoints() {
const baseHost = this.options.host || 'http://localhost:8080';
const normalize = (path: string) =>
baseHost.replace(/\/+$/, '') + '/' + path.replace(/^\/+/, '');

const chatBase = normalize('api/ai/chat');

return {
completion: chatBase + '/completion', // 普通模式
stream: chatBase + '/stream', // 流式模式
streamSimple: chatBase + '/stream-simple',
streamConfig: chatBase + '/stream-config',
session: normalize('api/ai/session'),
platforms: normalize('api/ai/platforms'),
fileExtraction: normalize('api/ai/file/extraction')
};
}

private sendMessage(text: string): void {
if (this.options.stream) {
this.sendStreamMessage(text); // 流式模式
} else {
this.sendNormalMessage(text); // 普通模式
}
}

🎨 主题定制系统

渐进式主题方案

我们设计了三层主题定制方案:

第一层:预设主题

1
2
3
new AIAgent({
theme: 'light' // 或 'dark'
});

第二层:自定义颜色

1
2
3
4
5
6
7
8
new AIAgent({
theme: 'light',
colors: {
primary: '#ff6b6b', // 主色调
background: '#f0f0f0', // 背景色
text: '#2c3e50' // 文本颜色
}
});

第三层:完整配色方案

1
2
3
4
5
6
7
8
9
10
11
12
new AIAgent({
colors: {
primary: '#ff6b6b',
primaryHover: '#ff5252',
background: '#f0f0f0',
text: '#2c3e50',
border: '#e0e0e0',
aiMessageBg: '#e8f5e9',
userMessageBg: '#ff6b6b',
headerBg: '#fafafa'
}
});

CSS 变量的优势

使用 CSS 变量而非直接修改样式的好处:

  1. 性能更好:浏览器原生支持,无需 JavaScript 逐个修改元素
  2. 优先级明确:CSS 变量继承机制清晰
  3. 响应式支持:可以配合媒体查询实现响应式主题
  4. 易于维护:修改一处,全局生效

🚀 打包与发布

Webpack 多产物输出

为了满足不同场景的需求,我们配置了三种输出格式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// webpack.config.js
module.exports = [
// 1. UMD 格式(未压缩)
{
output: {
filename: 'ai-agent.js',
library: { name: 'AIAgent', type: 'umd' }
}
},
// 2. UMD 格式(压缩)
{
output: {
filename: 'ai-agent.min.js',
library: { name: 'AIAgent', type: 'umd' }
},
optimization: {
minimize: true,
minimizer: [new TerserPlugin()]
}
},
// 3. ESM 格式(现代构建工具)
{
output: {
filename: 'ai-agent.esm.js',
library: { type: 'module' }
},
experiments: {
outputModule: true
}
}
];

TypeScript 类型声明

使用 ts-loader 自动生成类型声明文件:

1
2
3
4
5
6
7
8
9
module: {
rules: [
{
test: /\.ts$/,
use: 'ts-loader',
exclude: /node_modules/
}
]
}

生成的类型文件结构:

dist/
├── ai-agent.js           # UMD 格式(未压缩)
├── ai-agent.min.js       # UMD 格式(压缩)
├── ai-agent.esm.js       # ESM 格式
└── types/
    ├── ai-agent.d.ts     # 主入口类型声明
    └── types/
        └── index.d.ts    # 类型定义

NPM 包配置

1
2
3
4
5
6
7
8
9
10
11
12
{
"name": "ai-agent-plugin",
"version": "0.0.1",
"main": "dist/ai-agent.js", // CommonJS 入口
"module": "dist/ai-agent.esm.js", // ESM 入口
"types": "dist/types/ai-agent.d.ts", // TypeScript 类型
"files": [
"dist/",
"README.md",
"LICENSE"
]
}

🎯 跨框架使用实践

1. jQuery 项目

1
2
3
4
5
6
7
8
9
10
11
12
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="path/to/ai-agent.min.js"></script>

<script>
$(document).ready(function() {
const aiAgent = new AIAgent({
host: 'http://localhost:8080',
secret: 'your-api-key',
theme: 'light'
});
});
</script>

2. React 项目

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import React, { useEffect } from 'react';
import AIAgent from 'ai-agent-plugin';

function App() {
useEffect(() => {
const aiAgent = new AIAgent({
host: 'http://localhost:8080',
secret: process.env.REACT_APP_AI_SECRET,
theme: 'dark',
stream: true
});

return () => aiAgent.destroy();
}, []);

return <div>My App</div>;
}

3. Vue 3 项目

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<template>
<div>My App</div>
</template>

<script setup>
import { onMounted, onUnmounted } from 'vue';
import AIAgent from 'ai-agent-plugin';

let aiAgent = null;

onMounted(() => {
aiAgent = new AIAgent({
host: 'http://localhost:8080',
secret: import.meta.env.VITE_AI_SECRET,
position: 'bottom-left',
colors: {
primary: '#42b883' // Vue 绿
}
});
});

onUnmounted(() => {
aiAgent?.destroy();
});
</script>

🐛 踩坑与解决方案

问题 1:TypeScript 类型不兼容

现象:使用 Required<AIAgentOptions> 导致可选属性变必选

1
2
3
4
5
6
7
8
9
// ❌ 错误写法
private options: Required<AIAgentOptions>;

// 构建报错:colors 是可选的,但 Required 让它变成必选
this.options = {
host: '...',
secret: '...',
// colors 未提供,报错!
};

解决方案:直接使用原接口类型

1
2
// ✅ 正确写法
private options: AIAgentOptions;

问题 2:端口占用导致开发服务器无法启动

现象Error: listen EADDRINUSE: address already in use :::9000

解决方案:PowerShell 脚本清理占用端口

1
2
3
4
# 查找并停止占用 9000 端口的进程
Get-NetTCPConnection -LocalPort 9000 -ErrorAction SilentlyContinue |
Select-Object -ExpandProperty OwningProcess |
ForEach-Object { Stop-Process -Id $_ -Force }

问题 3:CSS 变量在 IE 中不生效

现象:IE 11 不支持 CSS 变量

解决方案:提供回退方案

1
2
3
4
.ai-agent-btn {
background: #4096ff; /* 回退值 */
background: var(--ai-agent-primary); /* CSS 变量 */
}

或者使用 PostCSS 插件自动生成回退:

1
2
3
4
5
6
7
8
// postcss.config.js
module.exports = {
plugins: [
require('postcss-custom-properties')({
preserve: false // 移除 CSS 变量,只保留回退值
})
]
};

问题 4:流式响应解析错误

现象:SSE 数据格式不规范导致解析失败

解决方案:增强容错处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
for (const line of lines) {
if (!line.trim() || !line.startsWith('data: ')) continue;

try {
const jsonStr = line.slice(6).trim();
if (jsonStr === '[DONE]') break; // 结束标记

const data = JSON.parse(jsonStr);
if (data.content) {
aiMessage += data.content;
}
} catch (err) {
console.warn('解析 SSE 数据失败:', line, err);
}
}

📊 性能优化

1. 代码分割与按需加载

虽然插件本身较小(~20KB),但仍可优化:

1
2
3
4
5
// 懒加载 Markdown 渲染库
private async renderMarkdown(text: string): Promise<string> {
const { marked } = await import('marked');
return marked(text);
}

2. 防抖与节流

输入框添加防抖,避免频繁触发:

1
2
3
4
5
6
7
8
9
10
11
private handleInput = debounce((text: string) => {
// 处理输入
}, 300);

function debounce(fn: Function, delay: number) {
let timer: number;
return function(...args: any[]) {
clearTimeout(timer);
timer = setTimeout(() => fn(...args), delay);
};
}

3. 虚拟滚动(未来规划)

对话消息过多时,使用虚拟滚动:

1
2
3
4
5
6
7
8
9
10
// 只渲染可视区域的消息
private renderVisibleMessages() {
const scrollTop = this.messagesEl.scrollTop;
const visibleHeight = this.messagesEl.clientHeight;

const startIndex = Math.floor(scrollTop / MESSAGE_HEIGHT);
const endIndex = Math.ceil((scrollTop + visibleHeight) / MESSAGE_HEIGHT);

return this.chatHistory.slice(startIndex, endIndex);
}

🔮 未来规划

短期目标(v0.1.0)

  • [ ] 支持 Markdown 渲染
  • [ ] 代码高亮显示
  • [ ] 消息历史持久化(LocalStorage)
  • [ ] 消息重发/编辑功能
  • [ ] 多语言支持(i18n)

中期目标(v0.2.0)

  • [ ] 语音输入/输出
  • [ ] 文件上传(图片、文档)
  • [ ] 多会话管理
  • [ ] 自定义快捷指令
  • [ ] WebSocket 支持

长期目标(v1.0.0)

  • [ ] 插件市场(第三方扩展)
  • [ ] AI 能力扩展(绘画、搜索、代码执行)
  • [ ] 团队协作功能
  • [ ] 移动端适配(React Native)
  • [ ] Electron 桌面应用

💡 经验总结

技术层面

  1. 选择合适的模块化规范:UMD 确实能实现跨框架兼容
  2. TypeScript 提升开发体验:类型安全减少 80% 的运行时错误
  3. CSS 变量是主题定制的最佳实践:灵活、高效、易维护
  4. 流式响应提升用户体验:实时反馈比加载动画更友好

工程层面

  1. 文档优先:README 写得好,issue 减少一半
  2. 示例丰富:提供 jQuery/React/Vue 示例,降低使用门槛
  3. 语义化版本:严格遵循 SemVer,避免破坏性更新
  4. 自动化测试(规划中):保证多框架环境下的稳定性

产品层面

  1. 保持简单:不要过度设计,先满足核心需求
  2. 渐进增强:功能分层,让用户自由选择
  3. 用户反馈驱动:根据 issue 和 PR 优化功能
  4. 社区运营:开源项目需要持续的社区互动

🙏 致谢

感谢以下开源项目的启发:

感谢所有给项目提 issue 和 PR 的开发者!


📚 参考资料


结语

从零打造一个跨框架通用插件是一次有趣的技术实践,它让我们深入理解了:

  • 模块化规范的本质
  • TypeScript 的工程化价值
  • CSS 架构的设计思想
  • 前端构建工具的工作原理

希望这篇文章能给正在开发跨框架组件的开发者一些启发。如果你有任何问题或建议,欢迎在 GitHub 上提 issue 讨论!

项目地址https://github.com/hujinbin/ai-agent
NPM 包npm install ai-agent-plugin
开源协议:MIT License


觉得有帮助?给个 ⭐ Star 吧!

Three.js 入门实战:基于 Vue3 做一个可拖拽的 3D 编辑器

Three.js 入门实战:基于 Vue3 做一个可拖拽的 3D 编辑器

前言

如果你是第一次接触 Three.js,这篇文章会带你从 0 到 1 建立完整认知:

  1. Three.js 到底是什么。
  2. 核心概念有哪些。
  3. 怎么在真实项目里用起来。
  4. 新手最容易踩哪些坑。

本文不是纯理论,而是结合我自己写的一个真实项目 drag-3D-three 来讲解。这个项目是一个 3D/2D 可视化大屏编辑器,Three.js 部分支持拖拽创建几何体、加载模型、选择元素、修改属性和保存案例。

技术栈:Vue 3 + TypeScript + Vite + Three.js + Pinia


一、Three.js 是什么

Three.js 是一个运行在浏览器中的 3D 图形库,它基于 WebGL 做了高级封装。

你可以把它理解成:

  1. WebGL 是底层图形 API,能力强但很底层。
  2. Three.js 是更易用的“3D 开发工具箱”。
  3. 你不需要直接写复杂着色器,也能快速搭建 3D 场景。

Three.js 适合做什么:

  1. 3D 数据可视化。
  2. 数字孪生大屏。
  3. 在线 3D 编辑器。
  4. 模型展示页。
  5. 小型网页 3D 互动。

二、先记住 7 个核心概念(新手必会)

1. Scene(场景)

所有 3D 对象都放在 Scene 里。它相当于“舞台”。

1
const scene = new THREE.Scene()

2. Camera(相机)

相机决定你从哪里看场景。

常用的是透视相机:PerspectiveCamera

1
2
const camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 1000)
camera.position.z = 50

3. Renderer(渲染器)

渲染器负责把场景画到 canvas 上。

1
2
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true })
renderer.setSize(width, height)

4. Geometry + Material + Mesh

一个能看到的 3D 物体通常由三部分组成:

  1. Geometry:形状(立方体、球体等)。
  2. Material:材质(颜色、金属感、粗糙度等)。
  3. Mesh:几何体 + 材质的组合。
1
2
3
4
const geometry = new THREE.BoxGeometry(5, 5, 5)
const material = new THREE.MeshStandardMaterial({ color: '#4F46E5' })
const mesh = new THREE.Mesh(geometry, material)
scene.add(mesh)

5. Light(光照)

如果你用的是标准材质(MeshStandardMaterial),必须有光,不然会很黑。

1
2
3
4
scene.add(new THREE.AmbientLight(0xffffff, 0.5))
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8)
directionalLight.position.set(1, 1, 1)
scene.add(directionalLight)

6. Controls(控制器)

项目里使用 OrbitControls,让用户能旋转、缩放、平移视角。

7. Animation Loop(动画循环)

Three.js 渲染通常放在循环里持续执行:

1
2
3
4
5
6
function animate() {
requestAnimationFrame(animate)
controls.update()
renderer.render(scene, camera)
}
animate()

只要你掌握这 7 点,已经能做出基础可交互场景。


三、基于我这个项目看 Three.js 怎么“落地”

下面结合我项目 drag-3D-three 的核心文件 src/components/ThreeDWorkspace.vue 看实战。

1. 初始化 3D 工作区

initThreeJS 里完成了完整初始化流程:

  1. 创建 scenecamerarenderer
  2. 注册 OrbitControls
  3. 添加环境光、平行光、网格辅助线 GridHelper
  4. 初始化 Raycaster 用于点击选中。
  5. 绑定 resizeclickdragoverdrop 等事件。

这就是一个标准的 Three.js 工程启动模板。

2. 元素数据结构(非常关键)

项目定义了统一元素结构,类似:

1
2
3
4
5
6
7
8
9
10
interface Element {
id: string
name: string
type: 'cube' | 'sphere' | 'cylinder' | 'pyramid' | 'custom'
position: { x: number; y: number; z: number }
size: { width: number; height: number; depth: number; radius: number }
color: string
number: string
modelUrl?: string
}

为什么新手要重视这一步:

  1. 数据结构统一后,渲染逻辑可复用。
  2. 能轻松做保存、撤销、模板化。
  3. 后续扩展动画、材质、权限都更顺。

3. 创建基础几何体

项目按 type 分发创建几何体:

  1. cube -> BoxGeometry
  2. sphere -> SphereGeometry
  3. cylinder -> CylinderGeometry
  4. pyramid -> ConeGeometry

然后统一材质和位置设置,最后 scene.add(mesh)

4. 加载自定义模型(GLTF/OBJ/FBX/STL)

项目支持多种模型格式,按扩展名选择对应 Loader:

  1. GLTFLoader
  2. OBJLoader
  3. FBXLoader
  4. STLLoader

加载后通过 Box3 计算模型包围盒,自动缩放到目标尺寸。这一步很实用,能避免不同模型源比例不一致的问题。

5. 用 Raycaster 做“点击选中”

点击画布后,先把鼠标坐标转换到标准化设备坐标,再发射射线检测相交对象。命中后通过 userData.elementId 找到业务元素。

这个思路是 Three.js 交互的基础技能。

6. 拖拽放置:把 HTML 拖拽和 3D 场景打通

项目中左侧元素卡片可拖拽,拖拽数据通过 dataTransfer 传入画布:

  1. 工具栏组件写入 application/json
  2. 工作区 handleDrop 解析 JSON。
  3. 根据鼠标位置生成 3D 元素并发出 element-created

这就是“低代码编辑器”的核心路径。


四、新手从零跑起来(实操步骤)

1. 安装和启动

1
2
npm install
npm run dev

2. 使用路径

  1. 进入编辑页。
  2. 左侧拖一个“立方体”到中间画布。
  3. 鼠标右键/滚轮配合 OrbitControls 调整视角。
  4. 点击元素,在右侧修改位置、尺寸、颜色、编号。
  5. 点击保存,写入案例库。

3. 你会学到什么

完成以上步骤后,你会自然掌握:

  1. Scene/Camera/Renderer 的协作关系。
  2. Three.js 和 Vue 组件的通信方式。
  3. 基础几何体与外部模型的统一管理。
  4. 射线拾取和拖拽交互。
  5. Three.js 项目中的状态持久化思路。

五、常见问题与避坑清单

1. 场景里什么都看不见

排查顺序:

  1. 相机是否对着物体。
  2. 是否有光照。
  3. 物体是否被加到 scene。
  4. 动画循环是否在执行。
  5. canvas 尺寸是否正确。

2. 模型加载失败

排查顺序:

  1. 模型路径是否可访问。
  2. 文件扩展名是否匹配 Loader。
  3. 是否有跨域问题。
  4. 控制台错误信息是否显示解析失败。

3. 拖拽位置不准

当前项目使用屏幕坐标线性映射,新手阶段够用;如果你追求更精确,建议升级为“射线与地面平面求交”方案。

4. 画面卡顿

常见原因:

  1. 每次数据变化都全量重建 mesh。
  2. 几何体分段数设置太高。
  3. 频繁创建对象但没有释放资源。

六、进阶建议:从教程走向实战

如果你已经跑通我这个项目,下一步建议按这个顺序升级:

  1. TransformControls(选中后可视化移动/旋转/缩放)。
  2. 实现元素分组和层级管理。
  3. 增加材质面板(透明度、贴图、发光)。
  4. 加载压缩模型(Draco)优化首屏。
  5. 增量更新渲染,减少全量重建。
  6. 引入截图功能生成案例缩略图。

七、总结

Three.js 入门最怕两件事:

  1. 只看 API,不做项目。
  2. 直接做复杂效果,基础不稳。

这篇教程给你的路径是:

  1. 先吃透核心概念(场景、相机、渲染器、光照、Mesh)。
  2. 用真实项目练习交互(拖拽、选中、更新、持久化)。
  3. 再逐步走向工程化(性能、架构、可扩展性)。

当你能独立搭出一个可拖拽编辑器时,就已经不再是 Three.js 初学者了。


项目求个 Star

如果这篇文章或这个项目对你有帮助,欢迎访问仓库并点一个 Star:

https://github.com/hujinbin/drag-3D-three

你的每一个 Star,都会让我更有动力持续更新:

  1. 新手友好的 Three.js 示例。
  2. 更完整的编辑器能力(变换控制、材质系统、分组层级)。
  3. 更实用的工程化优化(性能、资源管理、可维护性)。

也欢迎提 Issue 或 PR,一起把这个项目打磨成更好用的 Three.js 学习模板。


:D 一言句子获取中...