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,只要你能在入口文件编写自己的代码,就完全可以走这种开发模式