Vue/React与threejs如何解决冲突和卡顿-续
- 使用说明
- 核心思路
- 环境搭建(vue+three)
- vue运行机制分析
- 业务分离
- 使用threejs做背景
- 3D模块封装
- 使用ES6的Class来让逻辑性更强
- Threejs尽量按需引入
- 创建一个类
- 扩展写法
- 本次代码执行顺序
- 扩展内容
- 添加orbitControls和辅助线
- 解决事件覆盖
- 与Vue交互
- 添加操作代码
- 绑定ThreeCore到vue原型或window上
- 在vue中调用函数
- 协同开发
- 全框架可行性说明
- 源码文件已上传,还有有不懂的问题可以在下方留言
上一篇地址
不少人反映,上一篇中讲解的不够详细,或者难以看懂,这一篇我们从0开始来搭建一个Vue+Three的项目
使用说明
- 本方案适用于任何条件下任何框架,不受vue,react,angular等版本限制
- 本文着重讲解思路,部分代码可能不适用于TS
- 本篇教程中使用到了ES6的Class知识,对es6不熟的请优先补一下ES6的相关知识
- 本人并不熟悉vue3,所以部分写法比较倾向于vue2,这部分代码请各位自行调整
核心思路
- 彻底跳出vue的视界,在js层面来解决冲突
- vue只用来处理dom,threejs只用来处理画布内容
- 单例化并接口化three部分,参考前后端分离的 dom-canvas分离方案
环境搭建(vue+three)
这一部分基本上都是前端基本功,再不行百度一下也行
- 安装vue,这个过程本人就不解释了,本篇教程使用vue create vue-three来创建项目,本篇并不是在讲vue,所以vue的细节部分就不多说了,按照自己喜欢的配置即可
- 安装任意版本的Threejs, npm i three,本人使用当前最新版166
- node版本: 20.15.0
- npm版本: 10.8.1
- yarn版本: 1.22.22,本人主要使用yarn来安装依赖
- vue/cli版本 5.0.8
- 其他库版本
下面是本人的初始文件结构和package.json
vue运行机制分析
首先,我们现在关注这两个文件,一个是index.html,一个是main.js
任何的js程序,都应该有一个入口程序,而在vue中,入口程序不是 app.vue,而是main.js,我们来分析main.js的代码
createApp() 从字面意思上是创建vueApplication
然后参数中的mount() 用于获取页面中指定id的dom,也就是index.html中的 < div id=‘app’> 的这个div
也就是说,在mainjs中,vue的脚手架只干了一件事,从页面中读取到id为app的div,然后将vue文件编译后,生成dom,并填充这个div
我们把项目跑起来,用dom检查来检查这个div,内容基本上是由helloworld.vue文件提供
也就是说,vue其实并没有脱离html + css + js这个系统,而是用自己的系统单独在处理一个div
业务分离
既然是这样,那我们就可以完全视为原生的方式来开发,避免vue系统与threejs出现冲突
我们在main.js中,加入threejs的Helloworld的代码
//main.js import { createApp } from 'vue' import App from './App.vue' import * as THREE from "three"; createApp(App).mount('#app') let scene = new THREE.Scene() let camera = new THREE.PerspectiveCamera(50,window.innerWidth/window.innerHeight,0.1,2000); camera.position.z += 5; let renderer = new THREE.WebGLRenderer({ alpha:true }); renderer.setSize(window.innerWidth,window.innerHeight); document.body.appendChild(renderer.domElement); let geometry = new THREE.BoxGeometry(1,1,1); let material = new THREE.MeshBasicMaterial({ color:0xff0000 }); let mesh = new THREE.Mesh(geometry,material); scene.add(mesh); render(); function render() { renderer.render(scene,camera); requestAnimationFrame(render); mesh.rotation.x += 0.01; mesh.rotation.y += 0.01; }
复制
此时,我们发现,页面上出现了画布,也出现了跳动的方块,但是,位置不对
使用threejs做背景
这个问题非常简单,让app和threejs的内容均设定为absolute定位,然后threejs的canvas层级比id为app的div低即可
在app.vue中添加
#app { font-family: Avenir, Helvetica, Arial, sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; text-align: center; color: #2c3e50; margin-top: 60px; position: absolute;/*修改app的position*/ z-index: 10;/*建议大于10*/ }
复制
在main.js中添加,追加位置如图所示
//追加代码 renderer.domElement.style.position = "absolute"; renderer.domElement.style.top = "0"; renderer.domElement.style.left = "0"; //追加自适应代码 //自适应代码 window.addEventListener('resize',()=>{ renderer.setSize(window.innerWidth,window.innerHeight); camera.aspect = window.innerWidth/window.innerHeight; camera.updateProjectionMatrix(); });
复制
这时候再来看效果,基本上就已经做到了基本的融合了
3D模块封装
我们总不可能把所有的代码都挤到main.js中,一般来讲需要一个单独的文件来编写Threejs的部分代码,这里我们命名为Threecore
在src下新建一个文件夹 Threecore,新建一个js文件 Threecore.js
此时的文件结构
使用ES6的Class来让逻辑性更强
这里我们先上改完后的代码
//ThreeCore.js import { Scene, WebGLRenderer,Mesh, PerspectiveCamera, BoxGeometry, MeshBasicMaterial } from "three"; export default class ThreeCore { scene = new Scene(); renderer = new WebGLRenderer({ alpha:true }); camera = new PerspectiveCamera( 50,window.innerWidth/window.innerHeight,0.1,2000 ); /** * 构造函数,在new的时候会执行 */ constructor() { this.init(); this.addMesh(); } /** * 初始化代码 */ init(){ this.camera.position.z = 5; this.renderer.setSize(window.innerWidth,window.innerHeight); this.renderer.domElement.style.position = "absolute"; this.renderer.domElement.style.top = "0"; this.renderer.domElement.style.left = "0"; document.body.appendChild(this.renderer.domElement); } /** * 添加物体代码 */ addMesh(){ let geometry = new BoxGeometry(1,1,1); let material = new MeshBasicMaterial({ color:0xff0000 }); this.mesh = new Mesh(geometry,material); this.scene.add(this.mesh); } /** * 重置画布大小 */ resize = ()=>{ this.renderer.setSize(window.innerWidth,window.innerHeight); this.camera.aspect = window.innerWidth/window.innerHeight; this.camera.updateProjectionMatrix(); } /** * 渲染函数,这里本人为了干净和逻辑整洁,把requestAnimationFrame写到了main.js中 */ render = ()=>{ this.renderer.render(this.scene,this.camera); //编码习惯,使用前判定是否为null,这里的执行频率很高,可能会导致大量报错刷屏 if(this.mesh){ this.mesh.rotation.x += 0.01; this.mesh.rotation.y += 0.01; } } }
复制
//main.js import { createApp } from 'vue' import App from './App.vue' import ThreeCore from "@/ThreeCore/ThreeCore"; createApp(App).mount('#app') let threeCore = new ThreeCore(); render(); function render() { threeCore.render(); requestAnimationFrame(render); }
复制
Threejs尽量按需引入
Threejs本身文件很大,按需引入可以一定程度上降低打包出来的js文件的大小,所以我们在新的写法中做了按需引入
创建一个类
export default class ThreeCore{} 有了这一行之后,在main.js中,就可以
import ThreeCore from “@/ThreeCore/ThreeCore”;
并new出来
let threeCore = new ThreeCore();
new的时候,class系统会自动执行 constructor()函数,这个是类的功能
在类中,只能编写key和value这样的键值对,而不能直接编写代码,所以我们的操作代码都被归结在constructor函数中,并且用了**init()**来对逻辑进行区分,表示这一部分属于初始化阶段执行的代码,后续无需再次执行
扩展写法
当然,我们也可以把addMesh()写到main.js中,写法为:
threeCore.addMesh();
用new出来的实例,去调用它下面的函数addMesh()
本次代码执行顺序
//main.js
let threeCore = new ThreeCore()
//threecore.js
ThreeCore.constructor();
ThreeCore.init();
ThreeCore.addMesh();
//main.js
render();
扩展内容
添加orbitControls和辅助线
//ThreeCore import { Scene, WebGLRenderer, Mesh, PerspectiveCamera, BoxGeometry, MeshBasicMaterial, GridHelper } from "three"; import {OrbitControls} from "three/examples/jsm/controls/OrbitControls"; export default class ThreeCore { scene = new Scene(); renderer = new WebGLRenderer({ alpha:true }); camera = new PerspectiveCamera( 50,window.innerWidth/window.innerHeight,0.1,2000 ); //可以对controls进行声明,不用第一时间赋值 controls; /** * 构造函数,在new的时候会执行 */ constructor() { this.init(); this.addHelpers(); this.addEvent(); this.addMesh(); } /** * 初始化代码 */ init = ()=>{ //调整相机位置 this.camera.position.set(10,10,10); this.renderer.setSize(window.innerWidth,window.innerHeight); this.renderer.domElement.style.position = "absolute"; this.renderer.domElement.style.top = "0"; this.renderer.domElement.style.left = "0"; document.body.appendChild(this.renderer.domElement); //个人习惯,喜欢把orbitControls写到init中 this.controls = new OrbitControls(this.camera,this.renderer.domElement); this.controls.enableDamping = true;//开启阻尼效果 } addHelpers(){ let gridHelper = new GridHelper(10,10); this.scene.add(gridHelper); } addEvent = ()=>{ window.addEventListener('resize',this.resize); } /** * 添加物体代码 */ addMesh = ()=>{ let geometry = new BoxGeometry(1,1,1); let material = new MeshBasicMaterial({ color:0xff0000 }); this.mesh = new Mesh(geometry,material); this.scene.add(this.mesh); } /** * 重置画布大小 */ resize = ()=>{ this.renderer.setSize(window.innerWidth,window.innerHeight); this.camera.aspect = window.innerWidth/window.innerHeight; this.camera.updateProjectionMatrix(); } /** * 渲染函数,这里本人为了干净和逻辑整洁,把requestAnimationFrame写到了main.js中 */ render = ()=>{ this.renderer.render(this.scene,this.camera); //编码习惯,使用前判定是否为null,这里的执行频率很高,可能会导致大量报错刷屏 if(this.mesh){ this.mesh.rotation.x += 0.01; this.mesh.rotation.y += 0.01; } if(this.controls){ this.controls.update(); } } }
复制
两个新增的内容应该不用怎么解释了吧,页面效果
这里vue的dom跑到左边,是因为前面把定位改成了absolute,现在我们把位置调好
给app设定为100vw和100vh的宽高即可
/* app.vue的css部分 */ #app { width: 100vw; height: 100vh; font-family: Avenir, Helvetica, Arial, sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; text-align: center; color: #2c3e50; margin-top: 60px; position: absolute;/*修改app的position*/ z-index: 10;/*建议大于10*/ }
复制
解决事件覆盖
在常规的3d项目中,ui的部分一般都是全屏,很容易把画布的事件覆盖了,所以我们在最顶层的dom层级中,设定它的事件为无,这样所有它下面的事件都会变成无
pointer-events:none;
这个样式代码,一般是这样,只看父级
你的父组件是none,则子组件也是none,
你的爷爷组件是none,父组件是auto,那么子组件也是auto
如果爷爷组件是auto,父组件是none,那么子组件也是none
所以用这个样式来处理顶级dom后,记得处理子级dom的pointerEvents
/* app.vue中的css的代码 */ #app { width: 100vw; height: 100vh; font-family: Avenir, Helvetica, Arial, sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; text-align: center; color: #2c3e50; margin-top: 60px; position: absolute;/*修改app的position*/ z-index: 10;/*建议大于10*/ pointer-events: none; } #app>*{ pointer-events: auto; }
复制
与Vue交互
//ThreeCore.js追加代码 changeBoxColor = ()=>{ //注意,这里必须要引入 Three里面的Color //不要引入错了,类型错误的话也会报错 this.mesh.material.color = new Color(0xffffff * Math.random()) }
复制
//HelloWrold.vue重写代码 <template> <div class="hello"> <div @click="changeColor">变色</div> </div> </template> <script> export default { name: 'HelloWorld', props: { msg: String }, methods:{ changeColor(){ console.log(this); window.threeCore.changeBoxColor(); } } } </script> <!-- Add "scoped" attribute to limit CSS to this component only --> <style scoped> h3 { margin: 40px 0 0; } ul { list-style-type: none; padding: 0; } li { display: inline-block; margin: 0 10px; } a { color: #42b983; } </style>
复制
//main.js重写代码 import { createApp } from 'vue' import App from './App.vue' import ThreeCore from "@/ThreeCore/ThreeCore"; createApp(App).mount('#app') window.threeCore = new ThreeCore(); render(); function render() { window.threeCore.render(); requestAnimationFrame(render); }
复制
添加操作代码
我们只需要在ThreeCore下写一个函数即可
这里我们使用最简单的变色操作来演示
绑定ThreeCore到vue原型或window上
在vue2中,可以向原型上绑定一个对象,但是在vue3中,这个方案似乎不是很好用,本人并不是专门开发前端的,所以对vue3并不熟悉,所以绑定到原型的方式,就交给各位前端朋友了
最简单暴力的方式,就是绑定到 window上,虽然有被别人阅读源码和分析结构的风险,但是threejs本身高门槛,哪怕我代码放出来,你反编译了,你没有Threejs的基础也看不懂
window.threeCore = new ThreeCore();
复制
在window上的对象,在你的程序的任何地方都可以直接调用,相当于一个全局形式的函数
在vue中调用函数
由于本人并不了解vue3,所以采用了比较旧的vue2的编码风格
changeColor(){ console.log(this); window.threeCore.changeBoxColor();//直接全局调用即可 }
复制
协同开发
通过上面的方式,其实不难发现,我们已经将vue部分和three部分彻底的拆开了,vue只需要负责搞dom,three只负责渲染画布即可
如果你们有两个以上的人,完全可以参考这样的开发模式,一个人纯写threejs,另一个人纯写vue,这样做完全不会有任何的冲突,写threejs部分的人,只需要提供几个方便调用的函数给另一个人,这样可以大幅提高开发效率和合作能力
全框架可行性说明
从代码中,我们是把threejs的部分的代码,绑定到window上,且主入口也在main.js这种分离模式下,就可以看出,同样的react,也适用于这样的开发模式
基本上只要是基于html + css + js的技术的,其实都可以使用这种方式来开发
本质上,这种开发方式是一种原生开发,而非什么基于vue啊,基于react,也不是必须要webpack,rolllup这种环境开发,只要你能拿到three.module.js,只要你能在入口文件编写自己的代码,就完全可以走这种开发模式
源码文件已上传,还有有不懂的问题可以在下方留言
如有其他不懂的问题,可以在下方留言,也可以加入qq群咨询,本人的群于2024/7/8日正式创建,群号867120877,欢迎大家来群里交流