Vue3+Three.js实现

一、环境准备

1. 初始化 Vue3 项目

首先确保你已安装 Node.js(建议 v16+),然后通过 Vite 快速创建 Vue3 项目:

# 创建 Vue3 项目(选 Vue + TypeScript 或 JavaScript 均可)
npm create vite@latest vue3-three-demo -- --template vue
cd vue3-three-demo
npm install

2. 安装核心依赖

需要安装 Three.js 核心库和轨道控制器(Three.js 128+ 版本后控制器单独拆分):

# 安装 Three.js 核心 + 轨道控制器npm install three @tweenjs/tween.js
  • three:Three.js 核心库(3D 渲染核心)
  • @tweenjs/tween.js:可选,用于实现 3D 物体动画(本文会用到)

二、核心概念速览(必懂)

在写代码前,先记住 Three.js 3 个核心要素(缺少一个都无法渲染):

  • 场景(Scene):3D 世界的容器,所有 3D 物体、光照都要放在场景中
  • 相机(Camera):相当于人的眼睛,决定从哪个角度观察场景
  • 渲染器(Renderer):将场景和相机的内容渲染到页面 DOM 元素上
其他常用概念:
  • 几何体(Geometry):3D 物体的形状(如立方体、球体)
  • 材质(Material):3D 物体的外观(颜色、纹理、光泽)
  • 网格(Mesh):几何体 + 材质的组合(真正可渲染的 3D 物体)
  • 光照(Light):让 3D 物体有明暗层次(否则是纯色块)
  • 控制器(OrbitControls):让用户可以拖拽、缩放、旋转场景

三、编写第一个 3D 项目

创建 src/components/ThreeDemo.vue 组件,复制以下代码(含详细注释):

<template>
    <!-- 3D 渲染容器 -->
    <div class="three-container" ref="containerRef"></div>
</template>
<script setup lang="ts">
    import { ref, onMounted, onUnmounted } from 'vue';
    import * as THREE from 'three';
    import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
    import { Tween } from '@tweenjs/tween.js';
    // 核心对象引用
    const containerRef = ref<HTMLDivElement | null>(null);
    let scene: THREE.Scene, camera: THREE.PerspectiveCamera, renderer: THREE.WebGLRenderer;
    let cube: THREE.Mesh, controls: OrbitControls;
    // 初始化 3D 场景
    const initThree = () => {
    if (!containerRef.value) return;
    // 1. 创建场景(容器)
    scene = new THREE.Scene();
    scene.background = new THREE.Color(0xf0f0f0); // 场景背景色(浅灰)
    // 2. 创建相机(透视相机:模拟人眼视角)
    // 参数:视野角度(75°)、宽高比、近裁切面、远裁切面
    camera = new THREE.PerspectiveCamera(
    75,
    containerRef.value.clientWidth / containerRef.value.clientHeight,
    0.1,
    1000
    );
    // 设置相机位置(x, y, z),默认在原点(0,0,0),需要拉开距离才能看到物体
    camera.position.z = 5;
    // 3. 创建渲染器(WebGL 渲染器,高性能)
    renderer = new THREE.WebGLRenderer({ antialias: true }); // 开启抗锯齿
    renderer.setSize(containerRef.value.clientWidth, containerRef.value.clientHeight); // 设置渲染尺寸
    containerRef.value.appendChild(renderer.domElement); // 将渲染画布添加到 DOM
    // 4. 创建 3D 物体(立方体)
    // 几何体:BoxGeometry(宽, 高, 深)
    const geometry = new THREE.BoxGeometry(2, 2, 2);
    // 材质:MeshLambertMaterial(漫反射材质,需要光照才能显示)
    const material = new THREE.MeshLambertMaterial({
    color: 0x42b983, // 基础颜色(绿色)
    wireframe: false, // 是否显示线框(true 可看到几何体结构)
});
    // 网格:几何体 + 材质
    cube = new THREE.Mesh(geometry, material);
    scene.add(cube); // 将立方体添加到场景
    // 5. 添加光照(否则物体是纯色块,无明暗)
    // 环境光:均匀照亮所有物体,无阴影
    const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
    scene.add(ambientLight);
    // 方向光:模拟太阳光,有阴影效果
    const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
    directionalLight.position.set(5, 5, 5); // 光源位置
    scene.add(directionalLight);
    // 6. 添加轨道控制器(允许用户交互:拖拽旋转、滚轮缩放)
    controls = new OrbitControls(camera, renderer.domElement);
    controls.enableDamping = true; // 开启阻尼效果(旋转更顺滑)
    controls.dampingFactor = 0.05;
    // 7. 添加动画(立方体自动旋转)
    const animateCube = () => {
    // 使用 Tween 实现平滑旋转动画
    new Tween(cube.rotation)
    .to({ x: cube.rotation.x + Math.PI * 2, y: cube.rotation.y + Math.PI * 2 }, 5000)
    .repeat(Infinity) // 无限循环
    .start();
};
    animateCube();
    // 8. 监听窗口大小变化(自适应调整)
    window.addEventListener('resize', onWindowResize);
};
    // 窗口大小自适应
    const onWindowResize = () => {
    if (!containerRef.value) return;
    // 更新相机宽高比
    camera.aspect = containerRef.value.clientWidth / containerRef.value.clientHeight;
    camera.updateProjectionMatrix(); // 更新相机投影矩阵
    // 更新渲染器尺寸
    renderer.setSize(containerRef.value.clientWidth, containerRef.value.clientHeight);
};
    // 渲染循环(Three.js 核心,持续渲染场景)
    const renderLoop = () => {
    requestAnimationFrame(renderLoop); // 浏览器帧循环
    controls.update(); // 阻尼效果需要持续更新控制器
    Tween.update(); // 更新动画
    renderer.render(scene, camera); // 渲染场景和相机
};
    // 生命周期钩子:组件挂载时初始化
    onMounted(() => {
    initThree();
    renderLoop();
});
    // 生命周期钩子:组件卸载时清理(防止内存泄漏)
    onUnmounted(() => {
    window.removeEventListener('resize', onWindowResize);
    controls.dispose(); // 销毁控制器
    renderer.dispose(); // 销毁渲染器
    // 清理场景中所有物体
    scene.traverse((obj) => {
    if (obj instanceof THREE.Mesh) {
    obj.geometry.dispose();
    (obj.material as THREE.Material).dispose();
}
});
});
</script>
<style scoped>
    .three-container {
    width: 100vw;
    height: 80vh;
    margin: 20px 0;
}
</style>

2. 在主组件中使用

修改 src/App.vue,引入并使用 ThreeDemo 组件:

<template>
    <div id="app">
        <h1>Vue3 + Three.js 第一个 3D 项目</h1>
        <ThreeDemo />
    </div>
</template>
<script setup lang="ts">
    import ThreeDemo from './components/ThreeDemo.vue';
</script>
<style scoped>
    #app {
    text-align: center;
    padding: 2rem;
}
</style>

四、运行项目

npm run dev
打开浏览器访问 http://localhost:5173 即可

五、进阶优化:添加纹理贴图

如果想让立方体更真实,可以给它添加纹理贴图(替换之前的纯色材质)。

1. 准备纹理图片

在 public 文件夹下放入一张纹理图片(如 texture.jpg,可自行下载)。

2. 修改材质代码

将之前的 MeshLambertMaterial 替换为带纹理的材质:

// 加载纹理(Three.js 内置纹理加载器)
const textureLoader = new THREE.TextureLoader();
const texture = textureLoader.load('/texture.jpg'); // 图片路径(public 下直接写文件名)
// 纹理优化(可选)
texture.wrapS = THREE.RepeatWrapping; // 水平重复
texture.wrapT = THREE.RepeatWrapping; // 垂直重复
texture.repeat.set(2, 2); // 重复次数(x 轴 2 次,y 轴 2 次)
texture.anisotropy = renderer.capabilities.getMaxAnisotropy(); // 各向异性过滤(提升纹理清晰度)
// 带纹理的材质
const material = new THREE.MeshLambertMaterial({
    map: texture, // 纹理贴图
});
重新运行项目,你会看到立方体表面贴上了纹理图片,更具真实感。

六、关键 API 总结(快速查阅)

功能
核心 API
创建场景
new THREE.Scene()
透视相机
new THREE.PerspectiveCamera(fov, aspect, near, far)
WebGL 渲染器
new THREE.WebGLRenderer({ antialias: true })
立方体几何体
new THREE.BoxGeometry(width, height, depth)
漫反射材质
new THREE.MeshLambertMaterial({ color, map })
3D 物体(网格)
new THREE.Mesh(geometry, material)
环境光
new THREE.AmbientLight(color, intensity)
方向光
new THREE.DirectionalLight(color, intensity)
轨道控制器
new OrbitControls(camera, renderer.domElement)
纹理加载
new THREE.TextureLoader().load(url)
动画循环
requestAnimationFrame(renderLoop)
复杂几何体:尝试 SphereGeometry(球体)、CylinderGeometry(圆柱体)等;
高级材质:MeshPhongMaterial(高光材质)、MeshStandardMaterial(PBR 物理材质);
模型加载:使用 GLTFLoader 加载 3D 模型(.gltf/.glb 文件);
粒子系统:创建大量小物体实现粒子特效(如星空、烟雾);
后期处理:添加阴影、模糊、泛光等特效;
Vue 生态整合:结合 Pinia 管理 3D 状态,结合 Vue Router 实现多场景切换;

 

JavaScript工具类代码

1. 判断是否为移动端

function isMobile() {
    return /Mobi|Android|iPhone/i.test(navigator.userAgent);
}

2. 获取元素距离页面顶部的距离

function getOffsetTop(el) {
    let offset = 0;
    while (el) {
        offset += el.offsetTop;
        el = el.offsetParent;
    }
    return offset;
}

3. 防抖函数 debounce

function debounce(fn, delay = 300) {
    let timer;
    return function (...args) {
        clearTimeout(timer);
        timer = setTimeout(() => fn.apply(this, args), delay);
    };
}

4. 节流函数 throttle

function throttle(fn, delay = 300) {
    let last = 0;
    return function (...args) {
        const now = Date.now();
        if (now - last > delay) {
            last = now;
            fn.apply(this, args);
        }
    };
}

5. 复制文本到剪贴板

function copyToClipboard(text) {
    navigator.clipboard.writeText(text);
}

6. 平滑滚动到页面顶部

function scrollToTop() {
    window.scrollTo({top: 0, behavior: 'smooth'});
}

7. 判断对象是否为空

function isEmptyObject(obj) {
    return Object.keys(obj).length === 0 && obj.constructor === Object;
}

8. 数组去重

function unique(arr) {
    return [...new Set(arr)];
}

9. 生成随机颜色

function randomColor() {
    return `#${Math.random().toString(16).slice(2, 8)}`;
}

10. 获取 URL 查询参数

function getQueryParam(name) {
    return new URLSearchParams(window.location.search).get(name);
}

11. 判断是否为闰年

function isLeapYear(year) {
    return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
}

12. 数组乱序(洗牌算法)

function shuffle(arr) {
    for (let i = arr.length - 1; i > 0; i--) {
        const j = Math.floor(Math.random() * (i + 1));
        [arr[i], arr[j]] = [arr[j], arr[i]];
    }
    return arr;
}

13. 获取 cookie

function getCookie(name) {
    const match = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)'));
    return match ? decodeURIComponent(match[2]) : null;
}

14. 设置 cookie

function setCookie(name, value, days = 7) {
    const d = new Date();
    d.setTime(d.getTime() + days * 24 * 60 * 60 * 1000);
    document.cookie = `${name}=${encodeURIComponent(value)};expires=${d.toUTCString()};path=/`;
}

15. 删除 cookie

function deleteCookie(name) {
    setCookie(name, '', -1);
}

16. 判断元素是否在可视区域

function isInViewport(el) {
    const rect = el.getBoundingClientRect();
    return rect.top >= 0 && rect.left >= 0 && rect.bottom <= window.innerHeight && rect.right <= window.innerWidth;
}

17. 获取当前时间字符串

function getTimeString() {
    return new Date().toLocaleString();
}

18. 监听元素尺寸变化(ResizeObserver)

const ro = new ResizeObserver(entries => {
    for (let entry of entries) {
        console.log('size changed:', entry.contentRect);
    }
});
ro.observe(document.querySelector('#app'));

19. 判断浏览器类型

function getBrowser() {
    const ua = navigator.userAgent;
    if (/chrome/i.test(ua)) return 'Chrome';
    if (/firefox/i.test(ua)) return 'Firefox';
    if (/safari/i.test(ua)) return 'Safari';
    if (/msie|trident/i.test(ua)) return 'IE';
    return 'Unknown';
}

20. 监听页面可见性变化

document.addEventListener('visibilitychange', () => {
    if (document.hidden) {
        console.log('页面不可见');
    } else {
        console.log('页面可见');
    }
});

21. 判断图片是否加载完成

function isImageLoaded(img) {
    return img.complete && img.naturalWidth !== 0;
}

22. 获取元素样式

function getStyle(el, prop) {
    return window.getComputedStyle(el)[prop];
}

23. 监听粘贴事件并获取内容

document.addEventListener('paste', e => {
    const text = e.clipboardData.getData('text');
    console.log('粘贴内容:', text);
});

24. 判断字符串是否为 JSON

function isJSON(str) {
    try {
        JSON.parse(str);
        return true;
    } catch {
        return false;
    }
}

25. 生成指定范围的随机整数

function randomInt(min, max) {
    return Math.floor(Math.random() * (max - min + 1)) + min;
}

26. 监听窗口尺寸变化

window.addEventListener('resize', () => {
    console.log('窗口尺寸变化:', window.innerWidth, window.innerHeight);
});

27. 判断邮箱格式

function isEmail(str) {
    return /^[\w.-]+@[\w.-]+\.\w+$/.test(str);
}

28. 判断手机号格式(中国)

function isPhone(str) {
    return /^1[3-9]\d{9}$/.test(str);
}

29. 计算两个日期相差天数

function diffDays(date1, date2) {
    const t1 = new Date(date1).getTime();
    const t2 = new Date(date2).getTime();
    return Math.abs(Math.floor((t2 - t1) / (24 * 3600 * 1000)));
}

30. 监听键盘按键

document.addEventListener('keydown', e => {
    console.log('按下了:', e.key);
});

31. 获取页面滚动距离

function getScrollTop() {
    return window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop;
}

32. 判断是否为数字

function isNumber(val) {
    return typeof val === 'number' && !isNaN(val);
}

33. 生成唯一ID(时间戳+随机数)

function uniqueId() {
    return Date.now().toString(36) + Math.random().toString(36).substr(2, 5);
}

34. 监听鼠标右键菜单

document.addEventListener('contextmenu', e => {
    e.preventDefault();
    console.log('右键菜单被触发');
});

35. 判断是否为函数

function isFunction(val) {
    return typeof val === 'function';
}

36. 获取本地存储 localStorage

function getItemByLocal(key) {
    return localStorage.getItem(key);
}

37. 设置本地存储 localStorage

function setItemByLocal(key, value) {
    localStorage.setItem(key, value);
}

38. 删除本地存储 localStorage

function removeItemLocal(key) {
    localStorage.removeItem(key);
}

39. 判断是否为 Promise

function isPromise(obj) {
    return !!obj && (typeof obj === 'object' || typeof obj === 'function') && typeof obj.then === 'function';
}

40. 获取当前页面路径

function getPath() {
    return window.location.pathname;
}

41. 监听剪贴板复制事件

document.addEventListener('copy', e => {
    console.log('内容已复制到剪贴板');
});

42. 判断是否为数组

function isArray(val) {
    return Array.isArray(val);
}

43. 获取元素宽高

function getSize(el) {
    return {width: el.offsetWidth, height: el.offsetHeight};
}

44. 判断是否为布尔值

function isBoolean(val) {
    return typeof val === 'boolean';
}

45. 监听页面滚动事件

window.addEventListener('scroll', () => {
    console.log('页面滚动了');
});

46. 判断是否为对象

function isObject(val) {
    return val !== null && typeof val === 'object' && !Array.isArray(val);
}

47. 获取当前域名

function getHost() {
    return window.location.host;
}

48. 判断是否为空字符串

function isEmptyTirm(str) {
    return typeof str === 'string' && str.trim() === '';
}

49. 监听窗口获得/失去焦点

window.addEventListener('focus', () => console.log('获得焦点'));
window.addEventListener('blur', () => console.log('失去焦点'));

50. 判断是否为 DOM 元素

function isElement(obj) {
    return obj instanceof Element;
}

TypeScript中用for in遍历对象的属性

ts遍历对象报错误:

TS7053: Element implicitly has an ‘any’ type because expression of type ‘string’ can’t be used to index type ‘{ a: number; b: number; }’.   No index signature with a parameter of type ‘string’ was found on type ‘{ a: number; b: number; }’.

代码:

let arr = {
  a: 1,
  b: 2
};

for (const n in arr) {
  console.log(n, arr[n]);
}

解决办法为修改tsconfig.json,允许索引:

{
  "extends": "./tsconfig.json",
  "compilerOptions": {
    "suppressImplicitAnyIndexErrors": true,
    "outDir": "./out-tsc/app",
    "types": []
  },
  "files": [
    "src/main.ts",
    "src/polyfills.ts"
  ],
  "include": [
    "src/**/*.d.ts"
  ]
}

 

 

 

 

chroem 89+ A标签打开新窗口导致sessionStorage失效

localStorage作用范围:本地存储,关闭浏览器后,数据依然会保存。同源浏览器窗口可以共享使用localStorage存储的数据。

sessionStorage作用范围:只存在于当前会话页面,当会话结束后,数据也随之销毁,在不同的浏览器窗口中共享。也就是存在于当前浏览器页面,页面关闭,数据也会删除。(注意:通过鼠标右键打开的新标签无法共享sessionStorage)

而这块对于sessionStorage可能一直存在一个误区,在Chrome浏览器89版本前,当前会话页面指的是当浏览器窗口没有关闭时,窗口内同域网站可以共享此数据(同源浏览器多个窗口不共享),当页面全部关闭或窗口关闭后,sessionStorage数据会被摧毁,所以你用a标签跳转还是js跳转都会共享sessionStorage。

而在2021年3月初Chrome浏览器进行了批量更新,更新到89版本后,通过a标签target=”_blank”跳转到新页面时sessionStorage就会丢失。Chrome这一更新可能会导致很多网站的sessionStorage丢失,页面可能直接就崩掉了。

解决办法如下:

1、最简单的解决办法 – a标签添加属性 rel=”opener”

<a href="http://xxx" target="_blank" rel="opener">Link</a>

 

angular directives scope

以前学过,但是忘记了,所以重新记录一篇文章。

directive中的restrict属性,来决定这个指令是作为标签(E)、属性(A)、属性值(C)、还是注释(M)。

1、false(默认值):直接使用父scope。比较“危险”。

可以理解成指令内部并没有一个新的scope,它和指令以外的代码共享同一个scope。

2、true:继承父scope

 

 

3、{ }:创建一个新的“隔离”scope,但仍可与父scope通信

隔离的scope,通常用于创建可复用的指令,也就是它不用管父scope中的model。然而虽然说是“隔离”,但通常我们还是需要让这个子scope跟父scope中的变量进行绑定。绑定的策略有3种:

  • @:单向绑定,外部scope能够影响内部scope,但反过来不成立
  • =:双向绑定,外部scope和内部scope的model能够相互改变
  • &:把内部scope的函数的返回值和外部scope的任何属性绑定起来
(1)@:单向绑定

示例代码:

<body ng-app="watchDemo">
<!--外部scope-->
<p>父scope:<input type="text" ng-model="input"></p>
<!--内部隔离scope-->
<my-directive my-text="{{input}}"></my-directive>

<script>
    var app = angular.module('watchDemo', []);
    app.directive('myDirective', function () {
        return {
            restrict: 'E',
            replace: true,
            template: '<p>自定义指令scope:<input type="text" ng-model="myText"></p>',
            scope: {
                myText: '@'
            }
        }
    });
</script>
</body>

(2)=:双向绑定

示例代码:

<body ng-app="watchDemo">
<!--外部scope-->
<p>父scope:<input type="text" ng-model="input"></p>

<!--内部隔离scope-->
<!--注意这里,因为是双向绑定,所以这里不要“{{}}”这个符号-->
<my-directive my-text="input"></my-directive>

<script>
    var app = angular.module('watchDemo', []);
    app.directive('myDirective', function () {
        return {
            restrict: 'E',
            replace: true,
            template: '<p>自定义指令scope:<input type="text" ng-model="myText"></p>',
            scope: {
                myText: '=' // 这里改成了双向绑定
            }
        }
    });
</script>
</body>
(3)&:内部scope的函数返回值和外部scope绑定

 

<body ng-app="watchDemo">
<!--外部scope-->
<p>父scope:<input type="text" ng-model="input"></p>

<!--内部隔离scope-->
<!--注意这里,函数名字也要用 连字符命名法-->
<my-directive get-my-text="input"></my-directive>

<script>
    var app = angular.module('watchDemo', []);
    app.directive('myDirective', function () {
        return {
            restrict: 'E',
            replace: true,
            template: '<p>结果:{{ getMyText() }}</p>',
            scope: {
                getMyText: '&' // 这里改成了函数返回值的绑定
            }
        }
    });
</script>
</body>
12310
 
Copyright © 2008-2021 lanxinbase.com Rights Reserved. | 粤ICP备14086738号-3 |