Vue3 实战(三)

185 阅读2分钟

1. Vue3 生命周期

vs Vue2

生命周期Vue3Vue2
dom创建前setupbeforeCreated
dom创建后setupcreated
dom挂载前onBeforeMountbeforeMount
dom挂载后onMountedmounted
dom更新前onBeforeUpdatebeforeUpdate
dom更新后onUpdatedupdated
dom销毁前onBeforeUnmountbeforeDestory
dom销毁后onUnmounteddestoryed

示例

<template>
  <el-row :gutter="10">
    <el-col :span="12">
      <el-card>
        <ECharts :height="'200px'" :option="option" />
      </el-card>
    </el-col>
  </el-row>
</template><script lang="ts" setup>
import { ref } from 'vue';
import ECharts from '@/plugins/echarts/echarts.vue';
​
// 调用option值时
// const value = option.value;
const option = ref({});
​
onBeforeMount(()=>{ })
  
onMounted(()=>{ })
  
onBeforeUpdate(()=>{ })
  
onUpdated(()=>{ })
  
onBeforeUnmount(()=>{ })
  
onUnmounted(()=>{ })
</script><style lang="scss" scoped>
</style>
  1. script 中声明setup,代表dom创建前及dom创建时,并且也无需定义defineComponent,当然这样书写也就“丢掉”了this
  2. 自定义组件import后,可以直接在template中使用。
  3. ref函数结果在template直接使用,但在JavaScript或TypeScript中调用参数值时,需使用参数的“value”属性,类似这类函数还有reactive等,使用方法不尽相同。

2. ThreeJs

  三维仿真前端框架,技术本身并不难,但涉及3D建模知识就超出程序员的能力范围,所以这个工作建议交给专业人员!新手入门建议参考这个网址的指导。

ourjs.com/wiki/view/t…

示例

<template>
  <div id='virtual'></div>
</template>
​
<script lang='ts' setup>
import { onMounted, onBeforeUnmount } from 'vue';
import * as THREE from 'three';
import { CSS2DRenderer, CSS2DObject } from 'three/examples/jsm/renderers/CSS2DRenderer';
import { FBXLoader } from 'three/examples/jsm/loaders/FBXLoader';
import * as TWEEN from '@tweenjs/tween.js';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls';
import { Service } from './virtual.service';
import * as Entity from './virtual.entity';
import Header from "@/components/virtual/layout/header.vue";
​
// 渲染器
let webGLRenderer: any = null;
// 2D渲染图层
let css2DRenderer: any = null;
// 场景
let scene: any = null;
// 相机
let perspectiveCamera: any = null;
// 鼠标控制
let orbitControls: any = null;
// 交互点集合
let interactablePoints: Array<THREE.Sprite> = [];
// 交互点
let intersectPoint: any = null;
// 点击事件
const raycaster = new THREE.Raycaster();
// 鼠标位置
const vector2 = new THREE.Vector2();
// 获取3D模型(fbx)
const module = new Service().model();
// 交互点(接口获取)
const points = new Service().points();
​
onMounted(() => {
  // 渲染器
  webGLRenderer = new THREE.WebGLRenderer({ antialias: true })
  // 像素比
  webGLRenderer.setPixelRatio(window.devicePixelRatio);
  // 尺寸
  webGLRenderer.setSize(window.innerWidth, window.innerHeight);
  // 阴影效果
  webGLRenderer.shadowMap.enabled = true;
​
  const canvas = document.getElementById('virtual');
  if (!canvas) {
    throw new Error('Element is not defined');
  }
  canvas.appendChild(webGLRenderer.domElement);
​
  // 2D渲染图层
  css2DRenderer = new CSS2DRenderer();
  css2DRenderer.setSize(window.innerWidth, window.innerHeight);
  css2DRenderer.domElement.style.position = 'absolute';
  css2DRenderer.domElement.style.top = '0px';
  css2DRenderer.domElement.style.pointerEvents = 'none';
  document.body.appendChild(css2DRenderer.domElement);
​
  // 场景
  scene = new THREE.Scene();
  // 背景色
  scene.background = new THREE.Color(0x606266);
  // 雾化效果
  scene.fog = new THREE.Fog(0xeeeeee, 0, 100);
​
  // 相机(透视)(视场, 长宽比, 近面, 远面)
  perspectiveCamera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 1000);
  perspectiveCamera.position.set(120, 100, 100);
  perspectiveCamera.lookAt(new THREE.Vector3(0, 0, 0));
​
  // 半球光源(室外效果更自然)
  const boxGeometry = new THREE.BoxGeometry(0.001, 0.001, 0.001);
  // 用于暗淡不光亮表面的材质
  const meshLambertMaterial = new THREE.MeshLambertMaterial({ color: 0xffffff });
  // 网格(不渲染)
  const mesh = new THREE.Mesh(boxGeometry, meshLambertMaterial);
​
  // 平行光
  const directionalLight = new THREE.DirectionalLight(0xb5b1c1, 1);
  directionalLight.intensity = 1.2;
  directionalLight.position.set(20, 20, 5);
  directionalLight.castShadow = true;
  directionalLight.target = mesh;
  directionalLight.shadow.mapSize.width = 512 * 12;
  directionalLight.shadow.mapSize.height = 512 * 12;
  directionalLight.shadow.camera.top = 130;
  directionalLight.shadow.camera.bottom = - 80;
  directionalLight.shadow.camera.left = - 70;
  directionalLight.shadow.camera.right = 80;
  scene.add(directionalLight);
​
  // 环境光
  const ambientLight = new THREE.AmbientLight(0x605a64);
  scene.add(ambientLight);
​
  // 模型加载(.fbx(ACII))
  var fbxLoader = new FBXLoader();
  fbxLoader.load(
    module,
    (group) => {
      // Object3D遍历
      group.traverse(function (object3D) {
        // 光照是否有阴影
        object3D.castShadow = true;
        // 是否接收阴影
        object3D.receiveShadow = true;
      });
​
      group.rotation.y = Math.PI / 2;
      group.position.set(40, 0, -50);
      group.scale.set(1, 1, 1);
​
      // 对象组合
      const groupRender = new THREE.Group();
      groupRender.add(group);
​
      // 交互点
      points.forEach((item) => {
        let sprite = new Service().sprite();
        sprite.name = item.text;
        sprite.scale.set(1, 1, 1);
        sprite.position.set(item.coordinate.x, item.coordinate.y, item.coordinate.z);
​
        interactablePoints.push(sprite);
        groupRender.add(sprite);
      });
​
      scene.add(groupRender);
    },
    (progressEvent) => {
      if (Number(((progressEvent.loaded / progressEvent.total) * 100).toFixed(0)) === 100) {
        const source = new Entity.Coordinate(0, 10, 20), target = new Entity.Coordinate(0, 0, 0);
        new Service().animate(perspectiveCamera, orbitControls, source, target, 4000, () => { });
      }
    },
    (errorEvent) => {
      throw new Error(errorEvent.message);
    }
  );
​
  // 鼠标控制
  orbitControls = new OrbitControls(perspectiveCamera, webGLRenderer.domElement);
  orbitControls.target.set(0, 0, 0);
  orbitControls.enableDamping = true;
​
  window.addEventListener('resize', resize, false);
​
  webGLRenderer.domElement.style.touchAction = 'none';
  webGLRenderer.domElement.addEventListener('click', mouseClick, false);
  webGLRenderer.domElement.addEventListener('pointermove', mouseEnter, false);
​
  animate();
});
​
/**
 * VUE BeforeDestory事件
 */
onBeforeUnmount(() => {
  webGLRenderer.forceContextLoss();
  webGLRenderer.dispose();
  scene.clear();
});
​
/**
 * 屏幕尺寸变化
 */
const resize = () => {
  perspectiveCamera.aspect = window.innerWidth / window.innerHeight;
  perspectiveCamera.updateProjectionMatrix();
  webGLRenderer.setSize(window.innerWidth, window.innerHeight);
  css2DRenderer.setSize(window.innerWidth, window.innerHeight);
}
​
/**
 * 动画
 */
const animate = () => {
  // 收集每帧DOM操作,在一次重绘或回流中就完成
  // 重绘或回流的时间间隔跟随浏览器的刷新频率
  requestAnimationFrame(animate);
  webGLRenderer.render(scene, perspectiveCamera);
  css2DRenderer.render(scene, perspectiveCamera);
  TWEEN && TWEEN.update();
  orbitControls && orbitControls.update();
}
​
/**
 * 鼠标点击事件
 * 
 * @param event 事件
 */
const mouseClick = (event: any) => {
  // 鼠标位置
  vector2.x = (event.clientX / window.innerWidth) * 2 - 1;
  vector2.y = -(event.clientY / window.innerHeight) * 2 + 1;
​
  // 鼠标位置和当前相机矩阵计算raycaster
  raycaster.setFromCamera(vector2, perspectiveCamera);
  // raycaster和模型相交的数组集合
  const intersects = raycaster.intersectObjects(interactablePoints);
​
  // 清除展示点
  if (intersectPoint) {
    interactablePoints.map((item) => item.remove(intersectPoint));
    intersectPoint = null;
  }
​
  if (intersects.length > 0) {
    let intersect = intersects[0].object;
    const source = new Entity.Coordinate(intersect.position.x, intersect.position.y + 4, intersect.position.z + 12),
      target = new Entity.Coordinate(0, 0, 0);
    new Service().animate(perspectiveCamera, orbitControls, source, target, 1000, () => {
      let element = document.createElement('div');
      element.className = 'popup';
      element.innerHTML = intersect.name;
​
      let css2DObject = new CSS2DObject(element);
      css2DObject.position.set(0, 0, 0);
      css2DObject.name = intersect.name;
      intersectPoint = css2DObject;
​
      intersect.add(css2DObject);
    });
  }
}
​
/**
 * 鼠标置于画布事件
 * 
 * @param event 事件
 */
const mouseEnter = (event: any) => {
  vector2.x = (event.clientX / window.innerWidth) * 2 - 1;
  vector2.y = -(event.clientY / window.innerHeight) * 2 + 1;
  // 调整视角
  raycaster.setFromCamera(vector2, perspectiveCamera);
  // 获取相交点
  const intersects = raycaster.intersectObjects(interactablePoints, true);
  if (intersects.length > 0) {
    const intersect = intersects[0].object;
    const sprite = intersect as THREE.Sprite;
    sprite.material.color = new THREE.Color(0x337ab7);
  } else {
    interactablePoints.map((item) => {
      item.material.color = new THREE.Color(0x409eff);
    });
  }
}
</script>
​
<style lang="scss">
.popup {
  color: #ffffff;
  font-size: 14px;
  line-height: 30px;
  padding: 20px;
  border-radius: 5px;
  background: rgba(96, 98, 102, .9);
}
</style>

virtual.service.ts

import * as THREE from 'three';
import * as TWEEN from '@tweenjs/tween.js';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls';
import * as Entity from './virtual.entity';
​
export class Service {
  /**
   * 获取3D模型(fbx)
   *
   * @returns
   */
  model = () => {
    // TODO
    return '/src/components/virtual/models/model.fbx';
  };
  /**
   * 获取交互点
   *
   * @returns Array
   */
  points = () => {
    // TODO
    return [
      {
        id: '0001',
        text: 'Name: Mock Name A<br/>Key: Value',
        coordinate: { x: -2.8, y: 4.5, z: -0.7 },
      },
      {
        id: '0002',
        text: 'Name: Mock Name B<br/>Key: Value',
        coordinate: { x: -5.8, y: 2.5, z: -0.7 },
      },
      {
        id: '0003',
        text: 'Name: Mock Name C<br/>Key: Value',
        coordinate: { x: 5.8, y: 2.5, z: -0.7 },
      },
    ];
  };
  /**
   * 交互点,点击弹层
   *
   * @returns THREE.Sprite
   */
  sprite = (
    width: number = 100,
    height: number = 100,
    radius: number = 10,
    lineWidth: number = 5
  ) => {
    var canvas = document.createElement('canvas');
    canvas.width = width;
    canvas.height = height;
​
    const context = canvas.getContext('2d');
    if (!context) {
      throw new Error('Element is not defined');
    }
​
    context.beginPath();
​
    const x = (width + lineWidth) / 2,
      y = (height + lineWidth) / 2;
    context.arc(x, y, radius, 0, Math.PI * 2, true);
​
    context.closePath();
​
    // 背景色
    context.fillStyle = 'dark';
    context.fill();
​
    // 边框
    context.strokeStyle = 'white';
    context.lineWidth = lineWidth;
    context.lineCap = 'round';
    context.stroke();
​
    const texture = new THREE.Texture(canvas);
    texture.needsUpdate = true;
    const spriteMaterial = new THREE.SpriteMaterial({
      map: texture,
      transparent: true,
      opacity: 0.8,
    });
​
    return new THREE.Sprite(spriteMaterial);
  };
​
  /**
   * 动画
   *
   * @param perspectiveCamera 相机
   * @param orbitControls 鼠标控制
   * @param source 坐标点
   * @param target 坐标点
   * @param time 时间
   * @param callBack 回调
   */
  animate = (
    perspectiveCamera: THREE.PerspectiveCamera,
    orbitControls: OrbitControls,
    source: Entity.Coordinate,
    target: Entity.Coordinate,
    time: number,
    callBack: () => void
  ) => {
    const tween = new TWEEN.Tween({
      // 相机(x)
      x1: perspectiveCamera.position.x,
      // 相机(y)
      y1: perspectiveCamera.position.y,
      // 相机(z)
      z1: perspectiveCamera.position.z,
      // 控制点中心点(x)
      x2: orbitControls.target.x,
      // 控制点中心点(y)
      y2: orbitControls.target.y,
      // 控制点中心点(z)
      z2: orbitControls.target.z,
    });
​
    tween.to(
      {
        x1: source.x,
        y1: source.y,
        z1: source.z,
        x2: target.x,
        y2: target.y,
        z2: target.z,
      },
      time
    );
​
    tween.onUpdate(function (object) {
      perspectiveCamera.position.x = object.x1;
      perspectiveCamera.position.y = object.y1;
      perspectiveCamera.position.z = object.z1;
      orbitControls.target.x = object.x2;
      orbitControls.target.y = object.y2;
      orbitControls.target.z = object.z2;
      orbitControls.update();
    });
​
    tween.onComplete(function () {
      orbitControls.enabled = true;
      callBack();
    });
​
    tween.easing(TWEEN.Easing.Cubic.InOut);
    tween.start();
  };
}

virtual.entity.ts

/**
 * 三维坐标
 */
export class Coordinate {
  // X轴
  x!: number;
  // Y轴
  y!: number;
  // Z轴
  z!: number;
​
  constructor(x: number, y: number, z: number) {
    this.x = x;
    this.y = y;
    this.z = z;
  }
}

3. 总结

  Vue3 / TypeScript / Vite / Vue-Router / Vuex / Axios / Element-Plus / Echarts / ThreeJs 这些技术集成并没有多难,使用官网文档就足够,Vue3相较于Vue2的一些新特性,使用起来需要适应一下,注意:Vue3是向下兼容的,老系统升级也没必要过度求新,新手找个业务功能写写,熟能生巧。