threejs + vite + vue3 数字孪生简单案例

一、安装必要的软件

复制代码
3D项目创建:

npm create vite@latest task-hub -- --template vue 
npm create vite@latest:拉取最新版 Vite 脚手架创建项目
task-hub:项目文件夹名称(生成在当前目录)
--:分隔 npm 参数与 Vite 专属参数,不能省略
--template vue:指定模板为纯 Vue3(不含 TS、JSX 等)

npm install vue-router 安装路由
npm install three --save  # three 生产依赖
npm install @types/three -D  # 类型声明 开发依赖
npm install ant-design-vue     业务 UI 组件库
npm install typescript --save-dev  等价  npm install typescript -D  


npm install stats.js   //帧率监控,性能监控
npm install @types/stats.js -D  //开发依赖

// 运行依赖,页面要实际使用控制面板
Three.js/ WebGL 开发专用可视化调试控制面板,快速拖拽修改参数,不用反复改代码刷新页面。
专门调 Three.js、3D 场景、动画、光照、模型参数,普通用户不会看到这个面板。
npm install dat.gui
npm install @types/dat.gui -D

npm install gsap  补间动画

二、文件内容

文件结构如下:

复制代码
tian@hang:~/ThreejsEngineer$ tree -L 3
.
├── index.html
.......
├── package.json
├── package-lock.json
├── public
│   ├── favicon.svg
│   └── icons.svg
├── README.md
├── src
│   ├── App.vue
│   ├── assets
│   │   ├── css
│   │   └── img
│   ├── components
│   │   └── ThreeView.vue
│   ├── lesson
│   │   └── Home.vue
│   ├── main
│   │   └── main.js
│   └── router
│       └── index.ts
└── vite.config.js

tian@hang:~/ThreejsEngineer$ ls
index.html  node_modules  package.json  package-lock.json  public  README.md  src  vite.config.js

内容如下:

复制代码
package.json内容
{
  "name": "task-hub",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "preview": "vite preview --host"
  },
  "dependencies": {
    "ant-design-vue": "^4.2.6",
    "dat.gui": "^0.7.9",
    "gsap": "^3.15.0",
    "stats.js": "^0.17.0",
    "three": "^0.185.0",
    "vue": "^3.5.39",
    "vue-router": "^5.1.0"
  },
  "devDependencies": {
    "@types/stats.js": "^0.17.4",
    "@types/three": "^0.185.0",
    "@vitejs/plugin-vue": "^6.0.7",
    "@vue/tsconfig": "^0.9.1",
    "typescript": "^6.0.3",
    "vite": "^8.1.1",
    "vue-tsc": "^3.3.6"
  }
}


index.html内容:
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="./src/assets/img/TSK.png" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>工业系统</title>
  </head>
  <body>
    <div id="app"></div>
    <script type="module" src="./src/main/main.js"></script>
  </body>
</html>


main.js 内容
import { createApp } from 'vue'
import '../assets/css/style.css'
import App from '../App.vue'
createApp(App).mount('#app')


Home.vue 内容
<template>
  <div>
    <h2>Three.js 演示</h2>
    <ThreeView />
  </div>
</template>

<script setup lang="ts">
import ThreeView from '../components/ThreeView.vue'
</script>

App.vue内容
<script setup>
import Home from './lesson/Home.vue'
</script>
<template>
  <Home />
</template>


ThreeView.vue内容
<template>
  <!-- 专属容器,不再直接挂body -->
  <div ref="canvasContainer" style="width:100%;height:600px;border:1px solid #eee;"></div>
</template>

<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue'
import * as THREE from 'three'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
import { gsap } from 'gsap'

// DOM容器ref
const canvasContainer = ref<HTMLDivElement | null>(null)

// 全局场景变量,统一管理,卸载时销毁
let scene: THREE.Scene
let camera: THREE.PerspectiveCamera
let renderer: THREE.WebGLRenderer
let controls: OrbitControls
let cube: THREE.Mesh
let animateId: number
let gsapTween: gsap.core.Tween

// 窗口resize处理函数
const handleResize = () => {
  if (!canvasContainer.value) return
  const width = canvasContainer.value.clientWidth
  const height = canvasContainer.value.clientHeight
  camera.aspect = width / height
  camera.updateProjectionMatrix()
  renderer.setSize(width, height)
  renderer.setPixelRatio(window.devicePixelRatio)
}

// 双击全屏
const handleDblClick = () => {
  if (!document.fullscreenElement) {
    renderer.domElement.requestFullscreen()
  } else {
    document.exitFullscreen()
  }
}

// 渲染循环
const renderLoop = () => {
  animateId = requestAnimationFrame(renderLoop)
  controls.update()
  renderer.render(scene, camera)
}

// 挂载时初始化3D场景
onMounted(() => {
  if (!canvasContainer.value) return

  // 1. 场景
  scene = new THREE.Scene()

  // 2. 相机
  camera = new THREE.PerspectiveCamera(
    75,
    canvasContainer.value.clientWidth / canvasContainer.value.clientHeight,
    0.1,
    1000
  )
  camera.position.set(0, 0, 10)
  scene.add(camera)

  // 3. 立方体
  const boxGeo = new THREE.BoxGeometry(1, 1, 1)
  const mat = new THREE.MeshBasicMaterial({ color: 0xffff00 })
  cube = new THREE.Mesh(boxGeo, mat)
  scene.add(cube)

  // 4. 渲染器,挂载到vue容器,不是body
  renderer = new THREE.WebGLRenderer()
  renderer.setSize(canvasContainer.value.clientWidth, canvasContainer.value.clientHeight)
  canvasContainer.value.appendChild(renderer.domElement)

  // 5. 轨道控制器
  controls = new OrbitControls(camera, renderer.domElement)
  controls.enableDamping = true

  // 6. gsap动画
  gsapTween = gsap.to(cube.position, {
    x: 5,
    duration: 5,
    repeat: -1,
    yoyo: true,
    ease: "bounce.out",
    onComplete: () => console.log("动画完成"),
    onStart: () => console.log("动画开始")
  })

  // 7. 坐标轴辅助
  const axesHelper = new THREE.AxesHelper(8)
  scene.add(axesHelper)

  // 8. 监听事件
  window.addEventListener('resize', handleResize)
  renderer.domElement.addEventListener('dblclick', handleDblClick)

  // 启动渲染
  renderLoop()
})

// 组件销毁,释放资源(关键!防止内存泄漏)
onUnmounted(() => {
  // 停止渲染循环
  cancelAnimationFrame(animateId)
  // 停止gsap动画
  gsapTween.kill()
  // 移除监听
  window.removeEventListener('resize', handleResize)
  renderer.domElement.removeEventListener('dblclick', handleDblClick)
  // 销毁渲染器、几何体、材质释放显存
  renderer.dispose()
  cube.geometry.dispose()
  if (Array.isArray(cube.material)) {
    cube.material.forEach(m => m.dispose())
  } else {
    cube.material.dispose()
  }
})
</script>

三、服务器部署

(1)文件打包:npm run build

复制代码
tian@hang:~/ThreejsEngineer$ npm run build

> task-hub@0.0.0 build
> vite build

vite v8.1.2 building client environment for production...
✓ 21 modules transformed.
computing gzip size...
dist/index.html                   0.48 kB │ gzip:   0.34 kB
dist/assets/TSK-BnnPvGX4.png     10.62 kB
dist/assets/index-Bzyp_i7E.css    0.04 kB │ gzip:   0.06 kB
dist/assets/index-UVdWu6yE.js   671.57 kB │ gzip: 185.80 kB

[plugin builtin:vite-reporter] 
(!) Some chunks are larger than 500 kB after minification. Consider:
- Using dynamic import() to code-split the application
- Use build.rolldownOptions.output.codeSplitting to improve chunking: https://rolldown.rs/reference/OutputOptions.codeSplitting
- Adjust chunk size limit for this warning via build.chunkSizeWarningLimit.
✓ built in 672ms

(2)文件传输

/home/tian/ThreejsEngineer/dist/ :文件在主机的位置

kickpi@192.168.31.224:/home/kickpi/myvue3 :服务器用户名;ip地址、文件放置服务器的位置

复制代码
scp -r /home/tian/ThreejsEngineer/dist/ kickpi@192.168.31.224:/home/kickpi/myvue3

(3)权限配置

服务器第一次没起起来,检查时权限问题;做了如下操作

复制代码
sudo mkdir -p /www/myvue3
sudo cp -r /home/kickpi/myvue3/dist/* /www/myvue3/
sudo chown -R www-data:www-data /www/myvue3
sudo chmod -R 755 /www/myvue3

(4)安装服务器

用到的命令如下:

复制代码
sudo apt update
sudo apt install nginx -y         //安装服务器
nginx -v                          //查看版本
sudo systemctl start nginx       # 启动服务
sudo systemctl enable nginx      # 设置开机自启
sudo ufw allow 80/tcp        # 放行80端口    **kickpi不用**
sudo systemctl status nginx     # 查看运行状态
sudo systemctl reload nginx.service  #重新加载 ;更改文件后需要
sudo nginx -t               # 校验配置语法

(5)文件增加

sudo vim /etc/nginx/conf.d/taskhub.conf

复制代码
sudo vim /etc/nginx/conf.d/taskhub.conf
文件内容:
server {
    listen 5210;
    # 填你的域名或服务器公网IP
    server_name myvue3.com 192.168.31.224;
    # 上传dist的目录
    root /www/myvue3;
    index index.html;

    # 关键:Vue Router history模式刷新404解决方案
    location / {
        try_files $uri $uri/ /index.html;
    }

    # 静态资源缓存,提升加载速度
    location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff2)$ {
        expires 7d;
        add_header Cache-Control "public";
    }
}

四、内网访问展示