0%

Phaser3.js

1.什么是Phaser.js

Phaser 是一个快速、免费且有趣的开源 HTML5 游戏框架,可在桌面和移动 Web 浏览器中提供 WebGL 和Canvas 渲染,它是使用并依赖 Web 技术构建的。它创建的游戏旨在在桌面或移动浏览器或能够运行网络游戏的应用程序(如 Discord、SnapChat、Facebook 等)中玩。有多种方法可以使用第 3 方工具将浏览器游戏转换为本机移动或桌面应用程序,许多 Phaser 开发人员已经成功地做到了这一点。然而,Phasers 的主要关注点是,而且永远是 web。

Phaser 支持各种不同的物理系统,每个系统都充当可用于任何 Phaser 场景的插件。 如: Arcade Physics、Impact Physics 和 Matter.js Physics

2.核心配置config

对游戏进行一些列配置,如type、scene、 physics 、camera等

2.1 Scene

Arcade Physics(physics) :物理支持平台,在config 中先进行配置,方可在场景中设置对应模块组。在 Arcade Physics 中有两种类型的物理体,动态( Dynamic )和静态(Static)两种类型

动态物体: 可以通过速度或加速度等力四处移动的物体。它可以与其他物体反弹和碰撞,并且这种碰撞受物体和其他元素的质量影响。

静态物体: 仅具有位置和大小。它不受重力影响,你不能在它上面设置速度,当有东西与它碰撞时,它永远不会移动。名称为 static,本质上是 static。非常适合我们让玩家四处奔跑的地面和平台。

Group: 将相似对象组合在一起并将他们作为一个单元进行控制的方法,还可以通过Group与其他游戏对象之间的冲突。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
config = { // phaser配置
type: Phaser.AUTO,
physics: {
default: 'arcade',
arcade: {
gravity: { y: 300 },
debug: false
}
},
width: 800,
height: 600,
scene: this.scene
}
// 创建函数: 一般对scene的操作
function create(){
let platforms = this.physics.add.staticGroup(); // 创建一个静态组
this.platfroms.create(400,568,'ground').setScale(2).refreshBody() //torefreshBody() 是必需的,因为我们已经缩放了一个静态物理物体,因此我们必须告诉物理世界我们所做的更改。
platforms.create.(200,500,'ground') // 图像是根据其中心定位的
}




基础

材质

基类:Material

定义:材质描述了对象objects的外光,它们的定义方式与渲染器无关,如果使用不同的渲染器,则不必重写材质。

1702368339909

1702368442450

整体上来看,就是渲染表现能力越强,占用的计算机硬件资源更多。

  • 占用渲染资源 MeshBasicMaterial < MeshLambertMaterial < MeshPhongMaterial < MeshStandardMaterial < MeshPhysicalMaterial
  • 渲染表现能力 MeshBasicMaterial < MeshLambertMaterial < MeshPhongMaterial < MeshStandardMaterial < MeshPhysicalMaterial
材质或集合体共享

我们可以直接访问并修改物体的材质对象,因为材质事共享的如果对其进行修改该用到该材质的物体都会发生改变

我们同时也可以直接访问并修改物体的集合体对象,若有其他物体也是用到这个集合体 修改后也会造成影响。

1702370190692

三维变量的克隆和复制方法

let v1 = new three.Vector(1,2,3)

letv2 = v1.clone() //此时会克隆一个与v1一样属性与值得对象

let v3 = {}

v3.copy(v1)可将v1得值赋值给v3

若是物体得克隆 他的材质与集合体不单独进行克隆得话 那么材质和集合体还是存在共享问题

1702368996979

顶点:

顶点的每个坐标对应一个顶点法线 ,法线关系光照后阴影的角度。

物体的属性有一些列的属性:position,rotation,scalr等属性 还有相关的方法,其都会继承父级构造函数的属性和方法

向量:normalize 将向量转为单位向量 长度为1

1702368060275

组对象group

1702435513665

子对象:group得children属性下可以查看到这个组对象下面得所有子对象。

子对象任何变化始终都是根据着父节点进行的。我们也可以直接使用Object3D创建一个节点对象,也可以给网格添加子对象。

通过设置name 可以给相应的物体设置指定的名字。

Object3D.traverse遍历所有的子节点

Object3D.getObjectByName() 获取指定名称的节点

本地坐标:当前物体的的坐标就是position的位置

Object3D.postion 表示物体的局部位置

世界坐标:本地坐标加上所有上级的坐标累加坐标,是为子集物体始终是相对于父级物体进行偏移的

Object3D.getWordPosition(v3):将当前物体的三维坐标存在一个三维向量中,可以获取当这个物体的世界坐标

当我们通过translate修改了集合体顶点坐标的时候,该物体的局部坐标系并没有发生改变,所有我们对这个物体(object3d)的一些列操作(如rotate)这个物体模型会相对于坐标原点进行

物体隐藏visible:物体object3d 隐藏或显示一个模型。 隐藏父级物体 他的子集物体也会被隐藏

材质隐藏visible:材质materail 通过该属性可以控制是否隐藏该材质对应的模型对象。 相同材质的隐藏的话 所有使用到此材质的物体的模型都会被隐藏

object3d.remove: scene , mesh ,都有删除这个方法 与add添加模型对立,可以删除他的子代模型 如“senec.remove(light)删除环境光,因为光照也是通过add添加的 ,场景模型多的话可以遍历子代模型条件删除一些模型。在进行删除尝试中发现了一个微妙的问题,就是在遍历traverse中不建议直接删除当前的属性。会出现一些奇怪的问题

1702544424552

模型数的遍历

object3D.traverse() 遍历了下面的所有子子节点。

object3D.getObjectByName() 根据Object.name来查找对应的名字的模型

object3D.visible 控制模型的显示与隐藏

模型:gltf

包含内容:几乎包含了该模型的所有数据 模型的层级关系,rgb材质,纹理材质,骨骼动画,变形动画…

格式信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
{
"asset": {
"version": "2.0",
},
...
// 模型材质信息
"materials": [
{
"pbrMetallicRoughness": {//PBR材质
"baseColorFactor": [1,1,0,1],
"metallicFactor": 0.5,//金属度
"roughnessFactor": 1//粗糙度
}
}
],
// 网格模型数据
"meshes": ...
// 纹理贴图
"images": [
{
// uri指向外部图像文件
"uri": "贴图名称.png"//图像数据也可以直接存储在.gltf文件中
}
],
"buffers": [
// 一个buffer对应一个二进制数据块,可能是顶点位置 、顶点索引等数据
{
"byteLength": 840,
//这里面的顶点数据,也快成单独以.bin文件的形式存在
"uri": "data:application/octet-stream;base64,AAAAPwAAAD8AAAA/AAAAPwAAAD8AAAC/.......
}
],
}

gltf格式文件不一定就是以扩展名.gltf结尾,.glb就是gltf格式的二进制文件。比如你可以把.gltf模型和贴图信息全部合成得到一个.glb文件中,.glb文件相对.gltf文件体积更小,网络传输自然更快。

load(’模型地址’,加载成功后的函数,加载时的相关信息):我们可以在加载函数中再次处理这个模型,如修改模型的相关属性,可以通过第三个参数来设置相关进度条…

查询模型节点:getObjectName() 根据模型的name属性来查询谋和模型对象

遍历模型traverse:

查看gltf的默认材质

材质相同问题:模型材质共享的问题判断材质的name 是否相同,

解决方案:1,三维建模软件中设置,需要代码改变材质不要共享,要独享材质。2,代码批量更改,克隆材质对象,重新赋值给mesh的材质属性.

1
2
3
4
5
6
gltf.scne.getObjectByName('汽车').traverse(function(item){
if(item.isMesh){
item.material = obj.material.clone()
}
})

纹理

如果没有特殊的需求,一般为了正常渲染,避免色差,three.js中贴图的颜色空间编码属性encoding要与webGL渲染器的outputEncoding属性(最新版本中属性名已改为 outputColorSpace )保持一致。

纹理对象的encoding有多个属性:THREE.LinearEncoding(线性颜色空间:3000 ),THREE.sRGBEncoding (sRGB颜色空间:3001)

如果webGL的渲染器的色彩空间手动设置了后面的添加的纹理也需要设置成相关的色彩空间(最新版中纹理对象的色彩空间属性名已由encoding改为colorSpace)

给gltf模型更换颜色贴图:TextureLoader()加载新的模型数据 如果直接给gltf的模型材质map设置新的纹理会出现错位的情况,这和贴图的texture的翻转flipY属性有关。

纹理对象的flipY默认值为trun ,gltf的贴图flipY默认值为false,跟换模型的贴图时需要设置为false.

纹理的列阵wrapS wrapT 分别是在UV轴上的如何进行包裹。repeat在uv轴上重复的数量

环境贴图:

立方体纹理加载器cubetextureLoader

这个加载器加载六个图片作为空间的上下左右前后,这个用来加载环境贴图,

1
2
3
4
5
6
7
8
9
10
// 加载环境贴图
// 加载周围环境6个方向贴图
// 上下左右前后6张贴图构成一个立方体空间
// 'px.jpg', 'nx.jpg':x轴正方向、负方向贴图 p:正positive n:负negative
// 'py.jpg', 'ny.jpg':y轴贴图
// 'pz.jpg', 'nz.jpg':z轴贴图
const textureCube = new THREE.CubeTextureLoader()
.setPath('./环境贴图/环境贴图0/')
.load(['px.jpg', 'nx.jpg', 'py.jpg', 'ny.jpg', 'pz.jpg', 'nz.jpg']);
// CubeTexture表示立方体纹理对象,父类是纹理对象Texture

材质MeshStandardMaterial的envMap环境贴图属性,通过pbr材质的贴图属性反射周围的景物。

envMapIntensity属性控制了反光率 roughness:控制粗糙度

环境贴图的色彩空间编码应该与渲染器的保持一致

//pbr材质的相关属性:

clearcoat:可以给物体表面设置一层透明膜,就像一层透明的漆,范围在0-1.

clearcoatRoughness:0-1 ;清漆层的粗糙层度。

metalness: 0.9,//车外壳金属度
roughness: 0.5,//车外壳粗糙度

transmission 玻璃透明度

Ior:折光率

场景的背景:

透明度:new Three.WebGLRenderer({alpha:true})或 render.setColorAlpha:0-1

背景色:render.setClearColor(颜色,透明度)

导出图片:

1.创建a标签

2.设置a标签的herf的 为场景的canvas元素,可以通过render.domElement获取

3.渲染器设置preserveDrawingBuffer为true ,开启缓存。

4.通过canvas的toDataURL(‘image/png’)获取话不上的像素信息,将以png的格式保存。

mesh重叠时候会出现闪缩的情况叫做深度冲突(z-fighting)
自定义划线:顶点数据来画

几何体setFromPoints(v3)将v3顶点数据设为几何体的attribute.position的顶点位置属性。除了可以是三维向量数组还可以是二位向量数组。

曲线:基类cuver

1712822846881

管道几何体

1
TubeGeometry(path, tubularSegments, radius, radiusSegments, closed)

旋转成型几何体

LatheGeometry:通过一个圆形轮廓坐标点 旋转得到一个几何体曲面

1
2
LatheGeometry(points, segments, phiStart, phiLength)
points为一个二维矢量组成的一个线段 segments为细分数, phiStart开始角度 phiLength旋转的角度默认值2π

形状几何体

ShapeGeometry:通过一个集合的轮廓得到一个多边形几何体平面。

先通过 Shape 将这些点生成一个二维平面,shape([二维矢量点数组…])

用ShapeGeometry 将这个二位平面装维平面几何体 ShapeGeometry (shape)

多边形轮廓shape(重点)

使用一些列路径传入shape生成一个多边形的形状,它可以和ExtrudeGeometryShapeGeometry一起使用,获取点,或者获取三角,也可以获得一个该形状的几何体,非常Nice.

1
2
父类为Path,所以也是继承了父类的相关方法与属性,如画线,画圆,画椭圆,等等获取当前点等属性。
传入点组成opints,可构造一个自定任意形状的多边形

1713148838218

若线与圆弧组合的时候,第一次的结束点自动衍生至第二个圆弧的起点。当前一次线画完时会采取以当前的currentPoint为中心点。

shape的线与圆共同使用的时候,他们的起点会相连接

1713149897591

内孔:holes

在创建的shape内挖出对应的孔洞,这些孔洞的坐标位置相对原点。

1713153199017

边缘几何体

EdgesGeometry:给指定几何体添加外边框线查看其边缘。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
    // 集合体的边界线EdgesGeometry
const geometryModle = new THREE.CylinderGeometry(2,2,32)
const materialModle = new THREE.MeshLambertMaterial({
color: 0x004444,
transparent:true,
opacity:0.5,
})
const mesh2 = new THREE.Mesh(geometryModle,materialModle)
scene.add(mesh2)
const edgesGeometry = new THREE.EdgesGeometry(geometryModle,30)//第二次参数时发现角度大于 多少时才会渲染
const edgesMaterial = new THREE.LineBasicMaterial({
color: 0x00ffff,
})
const lineModle = new THREE.LineSegments(edgesGeometry,edgesMaterial)
scene.add(lineModle)
// 同理相关模型也可以在加载完成后也可以添加边缘线。
modelLoader.load('./bwm/scene.gltf',(gltf)=>{
gltf.scene.position.set(5,0,5)
gltf.scene.traverse((item)=>{
if(item.isMesh){
item.material = new THREE.MeshLambertMaterial({//设置新的材质属性
color: 0x004444,
transparent: true,
opacity: 0.5,
})
const itemEdge = new THREE.EdgesGeometry(item.geometry)//创建外框线
const itemLineMaterial = new THREE.LineBasicMaterial({//线性材质
color: 0x00ffff,
})
const itemLine = new THREE.Line(itemEdge,itemLineMaterial)
item.add(itemLine)
}
})
scene.add(gltf.scene)//模型添加到场景中

拉伸几何体

**ExtrudeGeometry :**也是通过一个平面坐标拉成一个几何体,

参数:

1
2
3
4
5
6
depth: 20,
bevelThickness: 5, //倒角尺寸:拉伸方向
bevelSize: 5, //倒角尺寸:垂直拉伸方向
bevelSegments: 20, //倒圆角:倒角细分精度,默认3
extrudePath:cuverpath//通过一个三维曲线让这个多变对其沿着这个曲线拉伸
steps:100//沿着路径细分精度,越大越光滑

1712907318826

弯曲水管流动效果:

贴图版本:创建一个贴图,然后将贴图设置一个阵列模式。再render函数中给贴图的x轴的点进行+1。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const pointsV = new THREE.CatmullRomCurve3([
new THREE.Vector3(20,10,5),
new THREE.Vector3(0,0,0),
new THREE.Vector3(-20,-15,-50),
])
const pipeBackgroundImage = new THREE.TextureLoader().load(img)
// pipeBackgroundImage.encoding = THREE.SRGBColorSpace
// 设置阵列模式
pipeBackgroundImage.wrapS = THREE.RepeatWrapping;
pipeBackgroundImage.wrapT = THREE.RepeatWrapping;
// uv两个方向纹理重复数量
pipeBackgroundImage.repeat.set(1,1);//注意选择合适的阵列数量
pipeBackgroundImage.encoding = THREE.SRGBColorSpace
console.log(pipeBackgroundImage,'tuoian');
const pipeline = new THREE.TubeGeometry(pointsV,65, 2, 25)
const pipeMaterial = new THREE.MeshPhysicalMaterial({emissive:'#3f7b9d',side:THREE.DoubleSide,map:pipeBackgroundImage,clearcoat:1})
const pipeMesh = new THREE.Mesh(pipeline,pipeMaterial)
scene.add(pipeMesh)
function animation(){
pipeBackgroundImage.offset.x += 0.0001; //uv动画
renderer.render(scene,camera)
requestAnimationFrame(animation)
}
animation(){

几何体的顶点属性

顶点位置数据geometry.attributes.position

顶点法向量数据geometry.attributes.normal

顶点UV数据geometry.attributes.uv

顶点颜色数据geometry.attributes.color

如何使用顶点颜色: 创建相关几何体时,通过设置起这个几何体顶点数量的颜色。

*若要添加顶点颜色需要在材质中开启vertexColors ,材质的color属性可以不用设置了

*rgb 范围1-0

1
2
3
4
5
6
7
const colors = new Float32Array([
1, 0, 0, //顶点1颜色
0, 0, 1, //顶点2颜色
0, 1, 0, //顶点3颜色
])
geotry.attributes.color = new three.BufferAttribute(colors,3) //三个一组

颜色的rgb范围如下图 ↓

1713170230091

绘制彩色曲线

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 1.创建一个三维贝塞尔曲线或者其他曲线
const peakCover = new three.CubicBezierCurve3(
new THREE.Vector3(4,4,4),
new THREE.Vector3(4,5,6),
new THREE.Vector3(7,6,6),
)

//2.获取这个曲线上的所有点位 .getpoints() 参数将曲线分割为多少个点
const peackPoints = peackCover.getpoints(20)

//3.设置这些点位的颜色
let peackColors = []
for(let i =0;i<peackPoints.length;i++){
const itemValue = i / peackPoints.length //等到这个点位对应的rgb
peackColors.push(itemValue,0,1)
}

//4.设置集合体的顶点数据
const peackCoverGeotry = new three.BufferGeometry().setFromPoints(new Float32Array(peackColors),3)

//5.创建曲线材质
const peackCoverMaterial = new three.LineBasicMaterial({
vertexColors:true,//使用顶点颜色渲染
// color:'red'
})

//创建线条
const coverLine = new three.Line(peackCoverGeotry,peackCoverMaterial)
scene.add(coverLine)

Color的使用

THREE.Color(r,g,b) 当所有参数被定义时,r是红色分量,g是绿色分量,b是蓝色分量。
当只有 r 被定义时:

  • 它可用一个十六进制 hexadecimal triplet 值表示颜色(推荐)。
  • 它可以是一个另一个颜色实例。
  • 它可以是另外一个CSS样式。例如:
    • ‘rgb(250, 0,0)’
    • ‘rgb(100%,0%,0%)’
    • ‘hsl(0, 100%, 50%)’
    • ‘#ff0000’
    • ‘#f00’
    • ‘red’
1
const color = new THREE.Color(r,g,b)
color的插值
1
2
3
4
5
6
7
8
9
10
11
//lerpColors
const c1 = new THREE.Color(0xff0000); //红色
const c2 = new THREE.Color(0x0000ff); //蓝色
const c = new THREE.Color();
// 颜色3.lerpColors(颜色1,颜色2,占比) 占比为两种颜色各占多少 但占比为0时到的颜色就是颜色1 占比为1时得到的颜色就是颜色2。
c = c.lerpColors(c1,c2,占比Value)
// lerp(颜色1,占比value)
c1.lerp(c2,1)// 颜色1.lerp(颜色2,占比) 占比为颜色1与颜色2的占比 占比为0为颜色1, 占比为2时为颜色2

let c3 = c1.clone().lerColor(c1,c2,占比)//这里是先克隆在用插值修改这个颜色

颜色插值的应用:不用自己用相关逻辑来写颜色的属性数组,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let peakCoverPoint = peakCover.getPoints(20)//获取曲线上的点
let colorArr = []
for(let i=0;i< peakCoverPoint.length ;i++){
const percent = i / peakCoverPoint.length; //点索引值相对所有点数量的百分比
//根据顶点位置顺序大小设置颜色渐变
// 红色分量从0到1变化,蓝色分量从1到0变化
colorArr.push(percent, 0, 1); //蓝色到红色渐变色
}
// 优化↓
let c1 = new three.Color(0x00ffff)
let c2 = new three.Color(0xffff00)
for(let i=0;i< peakCoverPoint.length ;i++){
const percent = i / peakCoverPoint.length; //点索引值相对所有点数量的百分比
//根据顶点位置顺序大小设置颜色渐变
let c = c1.clone(c2,percent)
colorArr.push(c.r, c.g, c.b); //颜色插值计算
}

模型的顶点及数据处理

引入的模型有些在返回的模型json对象的scne.children[0]中有geometry属性。有的会在其下面继续拓展多个物体。此时我们就需要遍历这个对象,去获取他的顶点数据。

BufferAttribute具有getX(), getY() ,getZ() 方法获取x(1维) y(2维) z(3维) 指定项的数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
//通过顶点数据 来将山体根据高度着色
modelLoader.load('shan2.glb',(gltf)=>{
gltf.scene.position.set(5,0,5)
console.log( gltf.scene);
gltf.scene.scale.set(25,25,25)

const zArr = [] //顶点数据组
//遍历所有物体的顶点将塞入zArr
gltf.scene.traverse(item=>{
let itemAttributes = item.geometry.attributes//顶点属性
let itemPosition = item.geometry.attributes.position//顶点位置数据
//在位置数据中有一个count表示这个物体有多少个顶点
for(let i = 0;i<itemPosition.length;i++){
zArr.push(itemPosition.getZ(i))
}
})
zArr.sort()//升序排序
let minHeight = zArr[0]//山脚高度
let maxHeight = zArr[zArr.length - 1]//山顶高度
let height = maxHeight - minHeight //山体的高度
let minColor = new THREE.Color(0x0000ff)//山脚颜色
let maxColor = new THREE.Color(0xff0000)//山顶颜色
//再次根据当前顶点个数进行循环 设置当前的顶点颜色
gltf.scene.traverse(item=>{
let itemAttributes = item.geometry.attributes//顶点属性
let itemPosition = item.geometry.attributes.position//顶点位置数据
let colorArr = []
for(let i = 0;i<itemPosition.count;i++){
let colorVal = (itemPosition.getZ(I) - minHeight) / height
let c = minColor.clone().lerp(maxColor,colorVal)
colorArr.push(c.r,c.g,c.b)
}
let float32Arr = new three.Float32Array(colorArr)
itemAttributes.geometry.color = new three.BufferAttribute(float32Arr,3)

item.material = new three.MeshLambertMaterial({
vertexColors:true//开启顶点颜色
})
})

})

1713253141908

相机camera

相机种类:正投影相机 OrthographicCamera ,透视投影相机 PerspectiveCamera ,立方体相机CubeCamera

正投影相机:相机所看到的是一个四边形衍生后的空间。 正投影没有透视效果,也就是不会模拟人眼观察世界的效果。

1
2
3
4
5
6
7
8
9
OrthographicCamera( left, right, top, bottom, near, far )
left 渲染空间的左边界
right 渲染空间的右边界
top 渲染空间的上边界
bottom 渲染空间的下边界
near near属性表示的是从距离相机多远的位置开始渲染,一般情况会设置一个很小的值。 默认值0.1
far far属性表示的是距离相机多远的位置截止渲染,如果设置的值偏小小,会有部分场景看不到。 默认值2000
left 与 top 为负值

相机的position位置若在物体位置左边范围里面 ,会渲染范围外的 ,涉及到范围的坐标的物体这回被切割一般

1713321813858

1713321203283

透视投影相机:具有一定角度的锥型视野空间。特点:模拟人眼所看到的物体特征

视觉差异:同样渲染一个物体,正投影渲染的没有透视相机看着的自然。

相机选择:对于大部分需要模拟人眼观察效果的场景,需要使用透视投影相机,比如人在场景中漫游,或是在高处俯瞰整个园区或工厂。

正投影没有透视效果,也就是不会模拟人眼观察世界的效果。在一些不需要透视的场景你可以选择使用正投影相机,比如整体预览一个中国地图的效果,或者一个2D可视化的效果。

WebGL渲染器更新Canvas画布尺寸

1
2
3
4
5
6
7
解释// onresize 事件会在窗口被调整大小时发生
window.onresize = function () {
const width = window.innerWidth; //canvas画布宽度
const height = window.innerHeight; //canvas画布高度
// 重置渲染器输出画布canvas尺寸
renderer.setSize(width, height);
};

也可以在创建的时候就设置render.setSize(window.innerWidth,window.innerHeight)

透视相机需要更新一下宽高比

1
2
3
4
5
6
7
window.onresize = function () {
// width、height表示canvas画布宽高度
const width = canvas.clientWidth //canvas画布宽度
const height = canvas.clientHeight //canvas画布高度
camera.aspect = canvas.clientWidth/canvas.clientHeight
camera.updateProjectionMatrix();
};

正投相机受到宽高影响left 和 right的影响。

1
2
3
4
const k = width / height; //canvas画布宽高比
const s = 50;
//控制left, right, top, bottom范围大小
const camera = new THREE.OrthographicCamera(-s * k, s * k, s, -s, 1, 8000);
1
2
3
4
5
6
7
8
9
10
11
12
13
// Canvas画布跟随窗口变化
window.onresize = function () {
const width = window.innerWidth; //canvas画布宽度
const height = window.innerHeight; //canvas画布高度
// 1. WebGL渲染器渲染的Cnavas画布尺寸更新
renderer.setSize(width, height);
// 2.1.更新相机参数
const k = width / height; //canvas画布宽高比
camera.left = -s*k;
camera.right = s*k;
// 2.2.相机的left, right, top, bottom属性变化了,通知threejs系统
camera.updateProjectionMatrix();
};

包围盒

包围一个物体外层包围的几何体,就如手办的透明收纳盒。

1
const box3 = new THREE.Box3();

expandByObject(mesh):方法获取这个物体的最小包围盒.

getSize(target:vector3):获取到这个包围盒的大小并赋值给一个v3.

getCenter(target:vector3):获取到这个包围盒的中心点,并赋给一个v3.

包围盒地图案例:

在这个案例中涉及到几个点

1.我们如何让地图居中,也就是摄像机如何对着地图,。

2.轨道控制器中心点对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
import cq from 'map.json' //地图的json数据

//地图使用的正投影相机
const width = canvas.clientWidth; //canvas画布宽度
const height = canvas.clientHeight; //canvas画布高度
const k = width / height; //canvas画布宽高比
const s = 5;//控制left, right, top, bottom范围大小
console.log(k*s,'ks');
const camera = new THREE.OrthographicCamera(-k*s,k*s,s,-s,0.001,8000)
camera.position.set(10,10,10)
camera.lookAt(0,0,0)
scene.add(camera)

let depth = 0.2//地图的深度
//生成区县模块以及区县的边框
cq.map(item=>{
let v2 = []
let v3 = []
item.path.forEach(el => {
v2.push(new THREE.Vector2(el[0],el[1]))
v3.push(new THREE.Vector3(el[0],el[1],depth+0.0001))
});

let shape = new THREE.Shape(v2)
let ShapeGeo = new THREE.ExtrudeGeometry(shape,{depth:depth,curveSegments:v3.length,bevelEnabled:false})
let shapeMaterial = new THREE.MeshPhongMaterial({color:'#0a4287',opacity:1})
let shapeMaterial2 = new THREE.MeshPhongMaterial({color:'#1b6ef3',opacity:1})
let shaeMesh = new THREE.Mesh(ShapeGeo,[shapeMaterial,shapeMaterial2])
shaeMesh.position.z = 0

const areaLineGeo = new THREE.BufferGeometry().setFromPoints(v3)
const areaLineMaterial = new THREE.MeshPhongMaterial({color:'#fff',})
const areaLine = new THREE.Line(areaLineGeo,areaLineMaterial)
console.log(shaeMesh);
scene.add(areaLine)
scene.add(shaeMesh)
})
console.log(cqoutLine);
//创建一个重庆地图轮廓 以便于让地图居中
let outLine = []
cqoutLine.features[0].geometry.coordinates.forEach(item=>{
console.log(item);
item[0].forEach(val=>{
outLine.push(new THREE.Vector3(val[0],val[1],depth+0.0001))
})
})
console.log(outLine,'新');
const outLineGeo = new THREE.BufferGeometry().setFromPoints(outLine)
const outLIneMaterial = new THREE.MeshBasicMaterial({color:'red'})
const outLineMesh = new THREE.Line(outLineGeo,outLIneMaterial)
// scene.add(outLineMesh)
//包围盒 通过包围盒获取到物体的中心点
camera.lookAt(0,0,0)
camera.position.set(300,300,300)
const mapBox = new THREE.Box3()
let outLineBox = mapBox.expandByObject(outLineMesh)
const center = new THREE.Vector3() //获取地图中心点
outLineBox.getCenter(center)
console.log(center,'center');
//重新设置相机的观察中心点以及相机的位置。
camera.lookAt(center.x,center.y,center.z)
camera.position.set(center.x,center.y,50)
//重新设置轨道控制器的中心点。需要手动update一下配置
Controls.target.set(center.x,center.y,center.z);
Controls.update();
let mapBoxSize = new THREE.Vector3()//获取地图包围盒的大小
outLineBox.getSize(mapBoxSize)
const obj = new THREE.Object3D()
obj.position.set(center.x,center.y,center.z)
cqLight.target = obj
1713348789513

相机的动画效果:

主要是通过移动相机的position 完成的。

如在animation中不断更新相机的位置,相机观察的中心点始终为某个点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function animation(){

ridius+=0.001

camera.position.z = r * Math.sin(ridius)

camera.position.x = r * Math.cos(ridius)

camera.position.x += 0.003

camera.position.z -= 0.003

camera.lookAt(5,1.2,-5)

renderer.render(scene,camera)

requestAnimationFrame(animation)

}

animation()

相机的**.up**属性控制哪个坐标轴朝上:

x轴朝上:camera.up.set(1,0,0)

y轴朝上:camera.up.set(0,1,0)

z轴朝上:camera.up.set(0,0,1)

管道穿梭案例:

思路创建一个曲线,生成对应的管道模型。获取这个曲线上的点,相机位置设置在第当前点位上 然后相机的朝向为当前点位的下一个点的位置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
//相机选择透视相机,相机的仰角调试大一点管道中看的更多。45设置为90
cosnt coverLine = new three.CubicBezierCurve3(
new THREE.Vector3( -10, 0, 0 ),
new THREE.Vector3( -5, 15, 0 ),
new THREE.Vector3( 20, 15, 0 ),
new THREE.Vector3( 10, 0, 0 )
)
//创建纹理
const textrue = new three.TextrueLoader().load('water.png')
textrue.warpS = three.RepeatWrapping //开启矩阵模式 水平平铺
textrue.repeate.x = 1

const coverMaterial = new three.MeshBasicMaterial({
map:textrue,
side: THREE.DoubleSide//双面显示
})

//获取曲线上的点
const coverPoints = coverLine.getPoints(500)

let i = 0
function animation(){
i+=1
if(i < coverPoints.length - 1){
camera.position.copy(coverPoints[i])//相机位置在当前项
camera.lookAt(coverPoints[i + 1])//相机朝向下一项
}else{
i = 0
}
renderer.render(scene,camera)
requestAnimationFrame(animation)

}

animation()

相机控件OrbitControls
  • 旋转:拖动鼠标左键。enableRotate:true 开启旋转
  • 缩放:滚动鼠标中键。 enableZoom :true 开启缩放
  • 平移:拖动鼠标右键 。enablePan:true 开启平移

设置控件的平移,缩放,旋转就是控制相机的位置变化。

如果修改了空间的target的目标点 则也修改了相机的camera。修改控件的属性时也需要更新控件update()

控制缩放范围:

1
2
3
4
5
6
透视投影相机看见为物体效果问近大远小,控件提供了两个属性minDistance 与 maxDistance, 
controls.minDistance = 5
controls.maxDistance = 15

正投影相机则是minZoom,maxZoom

控制相机的旋转范围:

1
2
3
4
5
6
7
8
控制相机旋转范围:
minPolarAngle / maxPolarAngle 控制相机的上下旋转范围
controls.minPolarAngle = 0
controls.maxPolarAngle = Math.PI / 4

minAzimuthAngle / maxAzimuthAngle 控制相机左右旋转范围
controls.minAzimuthAngle = 0
controls.maxAzimuthAngle = Math.PI /2

除了OrbitControls控件外 还有一个MapOrbitControls 模拟地图的拖拽。

光源:light

光源基类light:平行光:DirectionalLight 点光源:PointLight 聚光灯:SpotLight ;这光源三种都能产生阴影。

环境光源:AmbientLight 。不会产生阴影

调试注意:调试阴影的时候记得开启相机的辅助工具 以便快速找到相机的范围; 以及将光源可视化确定光源的具体位置。

光源如何产生阴影:

1.光源需要设置.castShadow 为true

2.产生阴影的模型需要设置.castShadow 为true

3.渲染器的允许渲染 renderder. shadowMap.enabled 为true

4.设置渲染范围.shadow.camera 属性值始终为正投影相机OrthographicCamera , 限制阴影的范围,如果给的不合适看不到或者看不全,模糊不清等

光源的shadow.mapSize

提高阴影的渲染效果,值越大越清楚。默认值512

1713778296664

注意:

在能覆盖包含阴影渲染范围的情况下, .shadow.camera的尺寸尽量小。

如果你增加.shadow.camera的长方体视野尺寸范围,阴影模糊齿感,可以适当提升.shadow.mapSize的大小。

光源的shadow.radius

改变阴影边缘的过度效果。

可以适当的提升.shadow.radius让边缘的过度效果更明显。或者说边缘逐渐弱化或者模糊化,没有明显的边界干。

1713778832957
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
 //光
// 设置渲染器,允许光源阴影渲染
renderer.shadowMap.enabled = true;
const light = new THREE.AmbientLight( 0x404040,10 ); // 柔和的白光
const gui = new GUI();//创建GUI对象
gui.add(light, 'intensity', 0, 100).name('环境光.intensity');
scene.add( light );
//平行光
const directionalLight = new THREE.DirectionalLight( 0xffffff,5)
directionalLight.position.set(100,100,100),
directionalLight.target.position.set(0,0,0)
directionalLight.castShadow = true
directionalLight.receiveShadow = true;
console.log('阴影相机属性',directionalLight.shadow.camera);
const directionalLightHelp = new THREE.DirectionalLightHelper(directionalLight,0xffffff)
directionalLight.shadow.mapSize.set(2540,2540) //边缘的颗粒度
directionalLight.shadow.radius = 5
directionalLight.shadow.camera.left = -100;
directionalLight.shadow.camera.right = 100;
directionalLight.shadow.camera.top = 100;
directionalLight.shadow.camera.bottom = -100;
directionalLight.shadow.camera.near = 0.01;
directionalLight.shadow.camera.far = 5000;

// scene.add(directionalLightHelp)
// 可视化平行光阴影对应的正投影相机对象
const cameraHelper = new THREE.CameraHelper(directionalLight.shadow.camera);
scene.add(cameraHelper);
// 可视化平行光
const dirHelper = new THREE.DirectionalLightHelper( directionalLight, 5);
scene.add( dirHelper );

const load = new GLTFLoader('模型1.gltf',gltf=>{
gltf.scene.castShadow = true
scene.add(gltf.scene)
})


// gui调节阴影
// 阴影子菜单
const shadowFolder = gui.addFolder('平行光阴影');
const cam = directionalLight.shadow.camera;
// 相机left、right等属性变化执行.updateProjectionMatrix();
// 相机变化了,执行CameraHelper的更新方法.update();
shadowFolder.add(cam,'left',-500,0).onChange(function(v){
cam.updateProjectionMatrix();//相机更新投影矩阵
cameraHelper.update();//相机范围变化了,相机辅助对象更新
});
shadowFolder.add(cam,'right',0,500).onChange(function(v){
cam.updateProjectionMatrix();
cameraHelper.update();
});
shadowFolder.add(cam,'top',0,500).onChange(function(v){
cam.updateProjectionMatrix();
cameraHelper.update();
});
shadowFolder.add(cam,'bottom',-500,0).onChange(function(v){
cam.updateProjectionMatrix();
cameraHelper.update();
});
shadowFolder.add(cam,'far',0,1000).onChange(function(v){
cam.updateProjectionMatrix();
cameraHelper.update();
});
// *做一个太阳的升起与降落:原理光源在绕着z轴进行旋转
let r = 100 //光源旋转半径
let angle = 0 //光源的角度
function animation(){
angle+=0.01
directionalLight.position.y = r * Math.sin(angle)
directionalLight.position.x = r * Math.cos(angle)

renderer.render(scene,camera)
requestAnimationFrame(animation)
}


精灵:sprite

spriteMaterial:材质与普通网格材质一样可以设置相关的.map envMap 啥的。

Sprite:始终朝向摄像机,旋转轨道也不会让精灵图旋转不会产生阴影。即使设置了castShadow为true也不会生效。与其他材质不同的是精灵不需要设置几何体 Geometry ,精灵图的默认尺寸都是 1 ,

透视相机中:也遵循远小近大的原理

正投影相机如何验证精灵的尺寸:如果精灵尺寸为1, 给相机top , buttom设置为0.5 则整个精灵回撑满画布。

精灵与矩形mesh区别:他们都是一个矩形,区别在于精灵始终平行于画布,而mesh的矩形则始终跟着旋转。在尺寸上,mesh可以通过geometry和mesh.scale来定义,然而精灵则只能通过sprite.scale来定义。

模拟下雨和下雪的效果:如果要做下雨效果3d的曲面几何体,一个曲面几何体需要4个三角形的话,如果有3千个水滴,则需要3万个三角形;如果用精灵的话,一个精灵只需要两个三角形,减少了很多三角形的生成。渲染性能要好很多。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
// const texture = new THREE.TextureLoader().load('img/wa3.jpg')
const texture = new THREE.TextureLoader().load('img/xue.png')
const spriteMaterial = new THREE.SpriteMaterial({
map:texture,
// rotation:Math.PI/4,//旋转精灵对象45度,弧度值
transparent:true,//SpriteMaterial默认是true
})
const group = new THREE.Object3D()
for(let i = 0;i < 6000;i++){
const sprite = new THREE.Sprite(spriteMaterial)
sprite.scale.set(5, 5, 1)
sprite.position.x = 500 * (Math.random() - 0.5)
sprite.position.y = 250 * (Math.random())
sprite.position.z = 500 * (Math.random() - 0.5)
group.add(sprite)
}
scene.add(group)

const clock = new THREE.Clock()
function loop(){
// 通过tranvers来循环修改比较卡
// group.traverse(item=>{
// console.log(item,'w');
// item.position.y -=0.1
// if(item.position.y<0){
// item.position.y = 100
// }
// })

// 根据时间来设置相关位置 很卡
// const t = clock.getDelta();// loop()两次执行时间间隔
// group.children.forEach(item=>{
// console.log(t);
// item.position.y -= t * 60
// if(item.position.y < 0){
// item.position.y = 100
// }
// })

// 周期性改变雨滴位置
group.children.forEach(item=>{
//近点的雨滴 雪比较大 可以将相机的近点参数设置到一点
item.position.y -= 0.1
item.material.rotation += 0.000001// 雪花旋转需要旋转材质中的rotation
if(item.position.y < 0){
item.position.y = 250
}
})
requestAnimationFrame(loop)
}
loop()
function animation(){

renderer.render(scene,camera)
requestAnimationFrame(animation)
}
animation()

后处理: EffectComposer

定义:可以理解为处理照片后期处理一个 ‘ps’。

这是一个拓展库需要单独引入EffectComposer.js 和相关的后期处理通道。

后期处理库:

  • OutlinePass.js:高亮发光描边
  • UnrealBloomPass.js:Bloom发光
  • GlitchPass.js:画面抖动效果
  • GammaCorrectionShader 伽马矫正

…等,通道可以组合使用:组合使用的话,先处理一个通道再处理另外一个。

threejs本地部署的服务文件包examples文件目录,全文检索关键词EffectComposer,可以找到后处理的很多案例。

设置后期处理后,模型会出现色差, renderer.outputEncoding色彩空间不会生效,此时我们需要设置伽马校正通道。GammaCorrectionShader.js 提供了伽马校正的对象,可以处理模型后处理后颜色的色彩。没有伽马矫正的通道 还需要引入一个shaderPass.js库 配合使用 将伽马校正的对象作为参数创建校验通道。

1714356663966

模型添加后会出现锯齿:后处理器多种抗锯齿配置, FXAAShader 和 SMAAShader。SMAA效果比FXAA的效果好写

如何使用呢:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
   import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';
import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';//渲染器通道RenderPass
import { OutlinePass } from 'three/addons/postprocessing/OutlinePass.js';//物体模型外描边通道RenderPass
import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js';// 引入UnrealBloomPass通道
// 引入GlitchPass通道
import { GlitchPass } from 'three/addons/postprocessing/GlitchPass.js';

// 伽马校正后处理Shader
import {GammaCorrectionShader} from 'three/addons/shaders/GammaCorrectionShader.js';
// ShaderPass功能:使用后处理Shader创建后处理通道
import {ShaderPass} from 'three/addons/postprocessing/ShaderPass.js';
// FXAA抗锯齿Shader
import { FXAAShader } from 'three/addons/shaders/FXAAShader.js';
// SMAA抗锯齿通道
import {SMAAPass} from 'three/addons/postprocessing/SMAAPass.js';

//首先需要使用后期处理器
const composer = new EffectComposer(renderder) //参数为需要处理的webgl渲染器

//创建渲染器通道。这个必须要有不然后面可能不会出现效果
const renderPass = new RenderPass(scene,camera)//参数为场景与相机
comporser.addPass(renderPass)//给后期处理器添加渲染器通道
//例子 物体的外边缘发光通道


//创建一个二维矢量 大小与画布一直
let v2 = new three.Vector2(canvas.innerWidth,canvas.innerHeight)
const outLinePass = new OutlinePass(v2,scene,camera)
outLinePass.selectedObjects = [mesh1] //selectedObjects属性为添加物体 值为数组所以可以添加多个
composer.addPass(outLinePass)

//outLinePass的相关属性
outLinePass.visibleEdgeColor.set(0xffff00) //控制描边的颜色
outLinePass.edgeThickness = 4 //描边的厚度
outLinePass.edgeStrength = 6 //描边的亮度
outLinePass.pulsPeriod = 2 //描边的闪烁频率

const bloom = new UnrealBloomPass(v2) //控制物体本身发光
bloom.strength = 2 //发光强度
// composer.addPass(bloom)

const glitchPass = new GlitchPass()//闪屏效果,类型以前黑白电视信号不好的闪屏
composer.addPass(glitchPass)

// 创建伽马校正通道
const gammaPass= new ShaderPass(GammaCorrectionShader);
composer.addPass(gammaPass);

//首先需要设置像素比,避免canvas画布数据模糊
renderer.setPixelRatio(window.devicePixelRatio);
const pixelRatio = renderer.getPixelRatio()
console.log(pixelRatio,'像素比');

//FXAA抗锯齿
const fxaa = new ShaderPass(FXAAShader)
fxaa.uniforms.resolution.value.x = 1/(canvas.clientWidth*pixelRatio)
fxaa.uniforms.resolution.value.y = 1/(canvas.clientHeight*pixelRatio)
// composer.addPass(fxaa)

//smaa抗锯齿
const smaa = new SMAAPass(canvas.clientWidth*pixelRatio,canvas.clientHeight*pixelRatio)
composer.addPass(smaa)

//还需要设循环渲染中调用一下后期渲染器 后期渲染器执行时也可以不用执行webgl渲染器的渲染了
function animation(){
//renderer.render(scene,camera)
composer.render()
requestAnimationFrame(animation)
}

animation()

射线Ray(重点)

射线起点:origin为一个三维向量

射线方向:direction 是一个三维向量单位,向量长度始终为1,在设置了三维向量后我们一般使用 .normalize() 将其向量进行归一化。归一化就是将这个线长度变为1。

射线的真实方向:射线方向归一后,得到的方向就是射线的防线,简单点来说我们将射线方向起点移动到射线起点就会得到射线的真实方向了。

intersectObjects(meshArr):与哪些物体相交。注意点:我们在执行这个方法前先更新一下坐标,因为修改了相关位置信息后会在下一次render时才生效,所以我们得updateMatrixWorld()手动更新.

17143751853581714375233084

屏幕坐标转three.js设备坐标:我们可以用offset与client来进行转换,两种坐标点转换逻辑其实都差不多。设备作标以canvas中心点为坐标原点,xy坐标的范围在[-1,1]。

计算逻辑:设备坐标范围[-1,1],所以我们需要一个范围。首先求出每个方向上的占比,此时得到的就是一个小于1

的小数。因为-1 到 1范围是2 ,所以得到的值乘2,再-1 结果永远都在 - 1 跟 1 之间了。唯一注意点是y轴方向相反 ,占比值取负*2 + 1。

offset转换:建议

1
2
3
//获取canvas画布的宽高 width,height
let x = (width/e.offsetX) * 2 - 1
let y = -(height/e.offsetY)*2 + 1

client转换:需要注意的是,client坐标是相对浏览器的,所以canvas画布距离浏览器上边与左边的距离需要自己删减一波,如果开全屏的画就不需要了。

1
2
3
4
5
6
7
8
9
   let canvasWidth = canvas.clientWidth
let canvasHeight = canvas.clientHeight
// 用client坐标转换设备坐标
let cX = ((e.clientX - 8)/canvasWidth) * 2 - 1
let cY = -((e.clientY-20.8-8)/canvasHeight) * 2 +1
console.log(cX,cY,'client');
//全屏
let cX = (e.clientX/canvasWidth) * 2 - 1
let cY = -(e.clientY/canvasHeight) * 2 + 1

1714442633565

射线拾取器Raycaster:创建Raycaster实例。他的ray属性就是射线,设置ray的origin,direction就是改变射线拾取器的起点与方向,也可以通过拾取器的Set方法来进行修改。

.intersectObject( mesh ):方法 判断是否包含某个物体

.intersectObjects( [ mesh1, mesh2 ] ):检索是否经过这些物体,将经过的物体返回成一个数组,没有就是空数组

.setFromCamera( v2 , camera) : v2作为方向, 在标准化设备坐标中鼠标的二维坐标 —— X分量与Y分量应当在-1到1之间。 射线所来源的摄像机。

.set(v3,direction): v3 作为起点,是个三维坐标。direction是一个方向也是三维坐标。

如果画布容器canvas尺寸发生变化,需要更新的设置:相机的宽高比需要更新camera.aspect=w/h ; 渲染器更新画布尺寸render.setSize(w,h) ; 相机参数变化,相机必须更新camera.updateProjectionMatrix();

精灵图也可以进行拾取,操作方法与mesh一样的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
	const ray = new THREE.Ray()
ray.origin = new THREE.Vector3(1,0,0)
// ray.direction = new THREE.Vector3(5,5,5).normalize()
ray.direction = new THREE.Vector3(5,5,5).normalize();
//数据归一化 将向量转化为方向向量 向量的长度变为1
// let L = ray.direction.length()
// ray.direction.multiplyScalar(1/L) //multiplyScalar将该方向的三维向量归一化 1除向量的长度得到这个比例,然后xyz三个点都会乘以这个值。

//判断射线是否相交某个三角形.intersectTriangle(v1,v2,v3,backfaceCulling:boolean,target) 相交时将值付给target.

//通过坐标拾取器来获取鼠标交互的模型
const raycaster = new Raycaster()
document.body.addEventListener('click',e=>{
let canvasWidth = canvas.clientWidth
let canvasHeight = canvas.clientHeight
//获取canvas画布的宽高 width,height
let x = (width/e.offsetX) * 2 - 1
let y = -(height/e.offsetY)*2 + 1
let v2 = new three.Vector2(x,y)
raycaster.setFromCamera(v2,camera) //更新一个起点为相机 方向为v2的射线。
const arr = rayCaster.intersectObjects([mesh3,mesh1,mesh2])//intersectObjects()经过其中物体中的哪些呢
if(arr.length > 0){
arr[0].object.material.color.set('red')//修改经过模型的颜色
}

// 如果画布的大小发生了改变那么 我们地重新设置相机的参数 及重新设置渲染器 画布大小 更新摄像机投影矩阵
document.querySelector('#change').addEventListener('click',()=>{
vue.flog = !vue.flog
if(vue.flog){
vue.dom.style.width = '500px'
vue.dom.style.height = '500px'
camera.aspect = canvas.clientWidth/canvas.clientHeight //设置相机宽高比
renderer.setSize(canvas.clientWidth,canvas.clientHeight)//设置画布大小
camera.updateProjectionMatrix()//更新摄像机投影矩阵。在任何参数被改变以后必须被调用。
// if(this.model&&this.model){
// console.log('处理');
// // this.model.renderer.setSize(500, 500);
// this.model.camera.aspect = 500 / 500;
// this.model.camera.updateProjectionMatrix()
// }
}else{
vue.dom.style.width = '400px'
vue.dom.style.height = '400px'
camera.aspect = canvas.clientWidth/canvas.clientHeight
renderer.setSize(canvas.clientWidth,canvas.clientHeight)
camera.updateProjectionMatrix()
// if(this.model){
// console.log('调用');
// // this.model.renderer.setSize(400, 400);
// this.model.camera.aspect = 400 / 400;
// this.model.camera.updateProjectionMatrix()
// }
}
})


})

html2D标注CSS2DRender/CSS2DObject

在某些场景中我们需要标注一些场景中的模块信息,此时我们需要自己设置信息的排版。CSS2DRender及CSS2DObject就可以实现。

原理:因为创建的始终为html,所以我们创建的css2dRender是一个html元素,但是这个元素要与three的画布的位置始终保持一致,这样通过CSS2DObject所创建的标注才不会偏移。

CSS2DRender:场景标注的渲染器,跟webgl渲染器的用法一致,但是需要注意位置与three画布保持一致

CSS2DObject: 将html转为网格模型的类,转化这个标注也可以设置相关的属性,如position等,因为基类是object3D

标签的位置设置:需要将标注添加到某个物体附近。1.将标注添加到该模型的子级;2.获取该模型的世界坐标也是可以的; 3. 标签模型对象作为需要标注mesh的子对象,然后获取mesh几何体某个顶点的坐标,作为标签模型对象局部坐标position;

标签偏移:可以通过设置标签的css定位属性来设置属性的偏移。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
const btn = document.querySelector('.tip2D')
const btn2 = document.querySelector('.tip3D')

const btn2D = new CSS2DObject(btn)
//如果需要将这个标注设置在 一个物体上要么获取物体的世界坐标系 或者将这个标注添加到这个物体里面作为子级。
let v3 = new THREE.Vector3(50,30,30)
// 将标注设置为世界坐标系位置
// boxMesh.getWorldPosition(v3)
// 将标注设置为定点位置
console.log( boxPosition.getX(0),'某个位置');
btn2D.position.copy(v3)
// scene.add(btn2D)

const glbLoader = new GLTFLoader()
let n = 5

const rayCaster = new Raycaster()
const renderPass = new RenderPass(scene,camera)
const composer = new EffectComposer(renderer)
composer.addPass(renderPass)
const outLinePass = new OutlinePass(new THREE.Vector2(canvas.clientWidth,canvas.clientHeight),scene,camera)
//模型描边颜色
outLinePass.visibleEdgeColor.set(0xffff00)
//高亮发光描边厚度
outLinePass.edgeThickness = 4
//高亮发光区域的强度
outLinePass.edgeStrength = 6
composer.addPass(outLinePass)
glbLoader.load('/零件.glb',gltf=>{
console.log(gltf,'模型');
gltf.scene.traverse(val=>{
if(val.isMesh||val.isLine){
// val.getWorldPosition(v3)
n+=30
if(n==185){
camera.position.set(n+10,n+10,n+10)
camera.lookAt(n,n,n)
controls.target.set(n,n,n)
controls.update()
console.log('相机的位置',camera);
}
val.position.set(n,n,n)
console.log(val.position,'???');
}
})
// btn.style.top = '20px'
// btn.style.left = '20px'
const tag2D = new CSS2DObject(btn)
console.log(tag2D,tag3D,'css对象');
let changeObj = null
//添加css2d标签
document.body.addEventListener('click',e=>{
let x = (e.offsetX/canvas.clientWidth)*2 - 1
let y = -(e.offsetY/canvas.clientHeight)*2 + 1
console.log(x,y);
let v2 = new THREE.Vector2(x,y)

rayCaster.setFromCamera(v2,camera)
let obj = rayCaster.intersectObjects(gltf.scene.children[0].children[0].children)
console.log(obj);
if(obj.length>0){
let a = obj[0].object
outLinePass.selectedObjects = [a]
changeObj = a
if(a.isLine) a.add(tag2D)//将标注添加到这个对象里面
}else{
// if(changeObj) changeObj.remove(tag)
// outLinePass.selectedObjects = []
}

})
//因为css2Drender 的事件被屏蔽了所以无法触发事件,我们可以单独给某个元素将事件触发开启
document.querySelector('.close1').style.pointerEvents = 'auto';
document.querySelector('.close1').addEventListener('click',()=>{
if(changeObj) changeObj.remove(tag2D)//关闭弹窗及外边缘发光
outLinePass.selectedObjects = []
})

scene.add(gltf.scene)

})


//css3D 渲染器
const css3DRender = new CSS3DRenderer()
css3DRender.domElement.style.position = 'absolute'//设置定位是让css3d这个元素与画布重叠
css3DRender.domElement.style.top = '0px'
css3DRender.domElement.style.pointerEvents = 'none'
css3DRender.setSize(canvas.clientWidth,canvas.clientHeight)
document.querySelector('.screen').appendChild(css3DRender.domElement)
window.addEventListener('resize',()=>{
css3DRender.setSize(canvas.clientWidth,canvas.clientHeight)
})

//css2D 渲染器
const css2Drender = new CSS2DRenderer()
css2Drender.domElement.style.position = 'absolute'//设置定位是让css3d这个元素与画布重叠
css2Drender.domElement.style.top = '0px'
css2Drender.domElement.style.pointerEvents = 'none'//关闭渲染的事件 避免相机或者场景无法点击的问题
css2Drender.setSize(canvas.clientWidth,canvas.clientHeight)
document.querySelector('.screen').appendChild(css2Drender.domElement)
window.addEventListener('resize',()=>{
//场景的大小也需要更新
css2Drender.setSize(canvas.clientWidth,canvas.clientHeight)
})


function animation(){
css2Drender.render(scene,camera)
css3DRender.render(scene,camera)
// renderer.render(scene,camera)
composer.render()
requestAnimationFrame(animation)
}
animation()

html3D标注CSS3DRender/CSS3DObject/CSS3DSprite

CSS3DRender与2D的使用基本上一致。不论是css2d还是3d 所创建的标注都不会被物体压住,因为他在一个canvas画布的一个html里面。

**2d与3d的区别:**2d始终朝上 就是普通的Html. 3d是可以跟着旋转与缩放的就如Mesh物体一样,css3d精灵则跟three精灵一样始终始终朝向相机, CSS3精灵模型CSS3DSprite尺寸、位置、缩放等渲染规律和CSS3对象模型CSS3DObject基本一致。

CSS2渲染的标签和CSS3渲染的标签偏移方式不同,CSS3标签,直接按照threejs模型尺寸修改方式改变,比用HTML像素方式更方便准确。

1
2
3
4
5
6
7
8
css2采取的是设置css定位属性,而css3采取的是网格的模型位置
css2:
const div = document.getElementById('tag');
// id="tag"元素高度322px,默认标签中心与标注点
div.style.top = '-161px'; //平移-161px,指示线端点和标注点重合
css3:
tag.scale.set(0.5,0.5,1);//缩放标签尺寸
tag.position.y += 10;//累加标签高度一半,标签底部和圆锥顶部标注位置重合
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
  
const btn2 = document.querySelector('.tip3D')
const glbLoader = new GLTFLoader()
let n = 5
const rayCaster = new Raycaster()
const renderPass = new RenderPass(scene,camera)
const composer = new EffectComposer(renderer)
composer.addPass(renderPass)
const outLinePass = new OutlinePass(new THREE.Vector2(canvas.clientWidth,canvas.clientHeight),scene,camera)
//模型描边颜色
outLinePass.visibleEdgeColor.set(0xffff00)
//高亮发光描边厚度
outLinePass.edgeThickness = 4
//高亮发光区域的强度
outLinePass.edgeStrength = 6
composer.addPass(outLinePass)
glbLoader.load('/零件.glb',gltf=>{
console.log(gltf,'模型');
gltf.scene.traverse(val=>{
if(val.isMesh||val.isLine){
// val.getWorldPosition(v3)
n+=30
if(n==185){
camera.position.set(n+10,n+10,n+10)
camera.lookAt(n,n,n)
controls.target.set(n,n,n)
controls.update()
console.log('相机的位置',camera);
}
val.position.set(n,n,n)
console.log(val.position,'???');
}
})
const tag3D = new CSS3DObject(btn2)
//禁止CSS3DObject标签对应HTMl元素背面显示
btn2.style.backface-visibility = 'hidden'

const tagSprite = new CSS3DSprite(btn2)
tag3D.scale.set(0.5,0.5,1);//缩放标签尺寸
tag3D.position.x += 60
console.log(tag2D,tag3D,'css对象');

let changeObj = null
//添加css2d标签
document.body.addEventListener('click',e=>{
let x = (e.offsetX/canvas.clientWidth)*2 - 1
let y = -(e.offsetY/canvas.clientHeight)*2 + 1
console.log(x,y);
let v2 = new THREE.Vector2(x,y)

rayCaster.setFromCamera(v2,camera)
let obj = rayCaster.intersectObjects(gltf.scene.children[0].children[0].children)
console.log(obj);
if(obj.length>0){
let a = obj[0].object
outLinePass.selectedObjects = [a]
changeObj = a
if(a.isLine) a.add(tag2D)
// if(a.isMesh) a.add(tag3D) //css3D 具有跟着缩放的特点,类似于Mesh ,始终是html创建的元素 旋转场景始终在模型上面。
if(a.isMesh) a.add(tagSprite) //css3D精灵 类似于精灵图,始终朝着向摄像机。
}else{
// if(changeObj) changeObj.remove(tag)
// outLinePass.selectedObjects = []
}

})
document.querySelector('.close1').style.pointerEvents = 'auto';
document.querySelector('.close1').addEventListener('click',()=>{
if(changeObj) changeObj.remove(tag2D)
outLinePass.selectedObjects = []
})
document.querySelector('.close2').style.pointerEvents = 'auto';
document.querySelector('.close2').addEventListener('click',()=>{
if(changeObj) changeObj.remove(tagSprite)
outLinePass.selectedObjects = []
})



scene.add(gltf.scene)

})


//css3D 渲染器
const css3DRender = new CSS3DRenderer()
css3DRender.domElement.style.position = 'absolute'//设置定位是让css3d这个元素与画布重叠
css3DRender.domElement.style.top = '0px'
css3DRender.domElement.style.pointerEvents = 'none'
css3DRender.setSize(canvas.clientWidth,canvas.clientHeight)
document.querySelector('.screen').appendChild(css3DRender.domElement)
window.addEventListener('resize',()=>{
css3DRender.setSize(canvas.clientWidth,canvas.clientHeight)
})

//css2D 渲染器
const css2Drender = new CSS2DRenderer()
css2Drender.domElement.style.position = 'absolute'//设置定位是让css3d这个元素与画布重叠
css2Drender.domElement.style.top = '0px'
css2Drender.domElement.style.pointerEvents = 'none'
css2Drender.setSize(canvas.clientWidth,canvas.clientHeight)
document.querySelector('.screen').appendChild(css2Drender.domElement)
window.addEventListener('resize',()=>{
css2Drender.setSize(canvas.clientWidth,canvas.clientHeight)
})


function animation(){
css2Drender.render(scene,camera)
css3DRender.render(scene,camera)
// renderer.render(scene,camera)
composer.render()
requestAnimationFrame(animation)
}
animation()

添加多i个标注

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
    let num = 0

for(let i=0;i<5;i++){
glbLoader.load('tree2.glb',glb=>{
console.log(glb,'shu');
glb.scene.traverse(item=>{
if(item.isMesh){
console.log(item,'shuju ');
// item.material.emissive.set('red')
}
})

glb.scene.scale.set(0.1,0.1,0.1)
glb.scene.position.set(num,0,num)

// 创建树的标签 css2D 如果需要将标签的位置设置在某个物体顶上的话 如果只设置css定位,缩放时标签会偏移;建议还是用精准定位标签的position属性
const treeDom = document.querySelector('.treeName').cloneNode()
treeDom.innerHTML = '第'+(i+1)+'棵树'
const treeTip = new CSS2DObject(treeDom)
//方案1:将标签添加到该模型中 单独设置y的坐标
// treeTip.position.y = 500
// glb.scene.add(treeTip)
//方案2:获取模型的世界坐标,在修改y的坐标
// let adr = new THREE.Vector3()
// glb.scene.getWorldPosition(adr)
// adr.y += 50
// treeTip.position.copy(adr)
// // treeTip.position.x = 50
// console.log(treeTip);
// scene.add(treeTip)


// 添加css3d精灵 css3d的精灵不能不能被物体遮挡
// const iconDom = document.querySelector('.icon').cloneNode()
// const iconSprite = new CSS3DSprite(iconDom)
// iconSprite.position.y = 400
// glb.scene.add(iconSprite)

// 添加精灵模型 精灵模型可以被物体遮挡。
// const textureLoad = new THREE.TextureLoader()
// let spriteImg = textureLoad.load('/警戒.png')
// const spriteM = new THREE.SpriteMaterial({map:spriteImg})
// const sprite = new THREE.Sprite(spriteM)
// sprite.position.y = 450
// sprite.scale.set(100,100,100)
// glb.scene.add(sprite)


//通过canvas来作为精灵的贴图绘制标注
let tipcanvas = createCanvas()
const canvasTexture = new THREE.CanvasTexture(tipcanvas)
const spriteM2 = new THREE.SpriteMaterial({map:canvasTexture})
const sprite2 = new THREE.Sprite(spriteM2)
sprite2.position.y = 450
sprite2.scale.set(canvas.width/canvas.height*100,100,100)
glb.scene.add(sprite2)

scene.add(glb.scene)
num+=20
})
}

function createCanvas(src='house/nz (1).png'){
const img = new Image()
img.src = src
//通过canvas来绘制精灵模型的贴图
const tipCan = document.createElement('canvas')
let str = '设备A'
let arr = str.split('')
const arc = /[\u4e00-\u9fa5]/
let strLength = 0 //字符串的长度
for(let i = 0;i<arr.length;i++){
if(arc.test(arr[0])){//判断的是否为汉字 是汉字+1
strLength+=1
}else{
strLength+=0.0 //是应为或者其他符号 则+0.5
}
}
// 根据字体符号类型和数量,文字font-size大小来设置canvas画布的宽度
const h = 80
const w = h + strLength*32
tipCan.width = w
tipCan.height = h

const h1 = h*0.8
const c = tipCan.getContext('2d')//获取canvas的上下文 才能绘制
const r = h1/2
c.arc(r,r,r,-Math.PI/2,Math.PI/2,true)//顺势针旋转半圆
c.arc(w-r,r,r,Math.PI/2,-Math.PI/2,true)//顺势针旋转半圆
// c.fillStyle = "rgba(0,0,0,0.0)"; //背景透明
// c.fillRect(0, 0, w, h);
c.fillStyle = "rgba(255,255,255,1)";
c.fill()
// c.drawImage(img, 0, 0, w, h);//图片绘制到canvas画布上
// 箭头
c.beginPath()
const h2 = h-h1
c.moveTo(w/2-h2*0.6,h1)
c.lineTo(w/2+h2*0.6,h1)
c.lineTo(w/2,h)
c.fill()
// 文字
c.beginPath()
c.translate(w/2,h1/2)
c.fillStyle = '#000000'//文本填充色
c.font = 'normal 32px 宋体'//文本的字体
c.textBaseline = 'middle'//文本与fillText定义的纵坐标,对齐线
c.textAlign = "center"; //文本居中(以fillText定义的横坐标)
c.fillText(str,0,0)

return tipCan
}

着色器材质(重要)

着色器是 WebGL 的重要组件之一, shader 它是一种使用 GLSL 语言编写的运行在 GPU 上的小程序。顾名思义,着色器用于定位几何体的每个顶点,并为几何体的每个可见像素进行着色 。着色器是屏幕上呈现画面之前的最后一步,用它可以实现对先前渲染结果进行修改,如颜色、位置等,也可以对先前渲染的结果做后处理,实现高级的渲染效果。

资源网站

nuxt入门 【前端部分】

使用nuxt开发toC应用,实现服务端渲染

课程目标:

  • nuxt是啥?
  • 搭建nuxt开发环境?
  • 前端
    • 路由搭建
      • layout…
    • 资源的使用
    • scss的使用
    • 数据的获取
  • 后端
    • 封装请求方法
    • 服务端渲染

安装

官网链接

  • node版本大于16.11
  • 安装pnpm
    • yarn global add pnpm
  • 设置host 映射ip
    • 打开目录 C:\Windows\System32\drivers\etc 修改hosts文件
1
185.199.108.133 raw.githubusercontent.com
  • 初始化
    • pnpm dlx nuxi init nuxt-demo
    • npx nuxi init
  • 进入项目 cd
  • 安装依赖 yarn
  • 运行项目

配置

官网链接

nuxt.config.ts

类似与vite项目的vite.config.ts, 亦或者是vue2项目的vue.config.ts。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
devtools: { enabled: true },
// 环境变量
runtimeConfig: {
// 服务端的环境变量
apiSecret: '123',
// 客户端的环境变量
public: {
apiBase: '/api'
}
},
vite: {
css: {
preprocessorOptions: {
scss: {
additionalData: `@import '@/assets/mixin.scss';`
}
}
}
},
// 配置px--》rem 啥的
postcss: {},
webpack: {}
})

其中环境变量,可以通过nuxt内置方法获取。

1
const runtimeConfig = useRuntimeConfig()

useRuntimeConfig无须导入直接使用即可

app.config.ts

当前的应用配置文件

1
2
3
4
5
6
7
8
9
export default defineAppConfig({
title: 'Hello Nuxt',
theme: {
dark: true,
colors: {
primary: '#ff0000'
}
}
})
1
const appConfig = useAppConfig()

使用内置方法获取

scss

配置全局scss

1
2
3
4
5
6
7
8
9
10
11
12
13
export default defineNuxtConfig({

vite: {
css: {
preprocessorOptions: {
scss: {
additionalData: `@import '@/assets/mixin.scss';`
}
}
}
}

})

跟vite的配置一样

reset.scss的使用

1
2
3
4
5
6
7
// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
devtools: { enabled: true },
css: [
'@/style/main.scss'
]
})

应用&组件&页面

官网链接

app.vue

是整个项目的入口文件

组件

component文件夹下的所有.vue文件。 创建之后,直接使用,无须导入。

页面

pages下的.vue文件就是页面。页面也是主动注册路由。

layout的使用

参考文档

layout的定义

在项目的layouts目录下定义各种layout即可。通过插槽传递内容

  • layouts/default.vue 一个默认的布局:default
  • layouts/customer.vue 一个名字叫做customer的布局

layout的使用

通过组件NuxtLayout配合NuxtPage 实现全局使用layout

1
2
3
4
5
6
7
<template>
<div>
<NuxtLayout name="custom">
<NuxtPage></NuxtPage>
</NuxtLayout>
</div>
</template>

app.vue。 当然你可以通过配置NuxtLayout的name指定非default的布局

definePageMeta的使用

经过以上步骤,项目中所有页面都应用了某个布局,如果某一个文件不想用。使用definePageMeta方法即可

1
2
3
4
5
definePageMeta({
layout: false
})


资源

nuxt的资源目录文件分为 public, assets.没什么好说的~~~

路由

官方链接

路由配置

路由参数设置

获取路由参数

路由中间件【路由守卫】

element-plus的使用

掘金参考

以前的项目

  • 下载element

  • 在main.ts中引入element, 样式文件

  • app.use(element)

nuxt进阶【服务端渲染】

服务端中间件编写

1
2
3
4
5
6
7
8
9
10
11
// 中间件,类似于koa的中间件,event类似于koa的ctx
export default defineEventHandler((event) => {
// console.log()
// console.log('New request: ' + event.node.req.url)
// 统一设置token
const app = useRuntimeConfig()
console.log(event)
event.context.baseUrl = app['BASE_URL']
})


fetch封装

页面使用

Vue3 使用文档 Composition API 组合式 api【重点】

课程介绍

vue3.2没有新增什么东西:

vue2: 数据(变化比较大,用法,底层原理),指令(没变),mustache(没变),methods(语法有所变化), 生命周期(语法和种类都有发生变化),计算属性,watch(变化比较大), 组件的封装(通信的语法有所变化),路由(变化的router构建的方式,配置方式没有发生变化,路由守卫都没有变化), 状态管理(vuex、pinia)

vue3 特性

  • Vue.js 3.0 “One Piece” 2019年发布

  • 新的响应式原理 ref reactive

  • diff 算法优化

    • Vue 2 中的虚拟 Dom 是全量比较。Vue 3 新增静态标记(PatchFlag)。在与数据变化后,与上次虚拟 DOM 节点比较时,只比较带有 PatchFlag 标记的节点。并且可以从 flag 信息中得知具体需要比较的内容。
    • 重写虚拟 DOM 的实现和 Tree-Shaking
  • 组合式 api(composition api) setup 函数 【99%代码都在里面写】

  • 新的计算属性 computed 新的侦听器 watch

  • hooks 语法 (当然是抄 react 的啦)

  • 生命周期变化

  • vue2 v-for 优先级高于 v-if vue3 v-if 优先级高于 v-for

  • v-model的本质变化

    • prop:value -> modelValue;
    • event:input -> update:modelValue;
  • .sync修改符已移除, 由v-model代替

  • 其他组件和 api 变化

main.js解读

vue2怎么项目,依赖于一个脚手架(@vue/cli),底层打包工具是webpack

vue3项目,使用vite创建(启动速度更快,配置更加的简单)

1
2
3
4
5
6
7
8
9
10
11
12
13
import Vue from 'vue'
import App from './App.vue'
import router from '@/router'
import store from '@/store'

Vue.config.productionTip = false

new Vue({
render: h => h(App),
router,
store
}).$mount('#app')

vue2创建项目

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* vue3.2
* - 函数式编程: (我要一个vue应用,我就调用一个函数,这个函数一执行,就给我返回一个vue应用)
*/

// createApp就是用来创建vue应用的
import { createApp } from 'vue'
// 导入样式
import './style.css'
// 根组件(最顶层的组件)
import App from './App.vue'

// createApp(App).mount('#app')
// 创建app
const app = createApp(App)

// 将vue应用app挂载到#app的节点下
app.mount('#app')

vue3项目的main.ts

总结:

vue2和vue3编程的思维不一样,其配套的一些插件的编程思维也发生变化了。

vue2面向对象,我要一个什么东西, new一个。周边的生态(我要一个router,new一个router)

vue3函数式编程 我要一个东西,这个东西一定配套有一个函数,这个函数一执行就能返回这个东西。

vscode报红解决思路

  • 你可以把严格模式关掉(不太推荐)

准备工作

添加别名@

  • vite.config.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#node没有内置ts 需要下载node的ts配置 不然无法识别path模块
// pnpm i @types/node -D

import {defineConfig} from 'vite'
import vue from '@vitejs/plugin-vue'
import {resolve} from 'path'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
'@': resolve(__dirname, 'src'),
},
},
})

修改tsconfig.json 增加@的ts识别

1
2
3
4
5
6
7
8
9
{
"compilerOptions": {
//++
"paths": {
"@/*": [
"./src/*"
],
}
},

vue3 router配置

创建路由文件

在src/router/index.ts。 该文件的功能如下:

  • 配置路由
  • 创建一个路由对象,然后将路由对象导出
  • 在main.ts中将路由对象和vue实例联系起来

路由使用三步骤

  • 建 【views 文件夹下面建立页面级别组件 如果是后台框架就在src文件夹下 建立layout文件夹】
  • 配 【router/index.ts 里面配置页面级别组件和路由地址的一一对应关系】
  • 测 【输入地址测试 要给出口】
    • 路由视图组件
    • 路由导航组件

router/index.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#createRouter 创建路由工厂函数
#createWebHashHistory 选用历史模式 vue3 历史模式刷新不报错
#createWebHistory 或者选用hash模式
#RouteRecordRaw 路由配置的约束
import { createRouter, createWebHashHistory,RouteRecordRaw } from "vue-router";

#静态引入路由组件
import 静态组件 from '组件地址'

#配置路由地址和页面级别组件的一一对应关系
const routes:Array<RouteRecordRaw> = [
{
path:'/路由地址',
component:()=>import('组件地址') #懒加载路由组件
},
{
path:'/路由地址',
component:静态组件
},



# 404 修改了 /:pathMatch(.*)匹配所有路由
{
path: '/:pathMatch(.*)',
redirect: '/404'
},
{
path: '/404',
component: () => import('@/views/not-found/index.vue'),
hidden: true,
}
]

#创建路由
const router = createRouter({
history: createWebHashHistory(), #hash模式
routes #导入路由配置
})

#导出配置好的路由
export default router

main.ts 导入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#导入 createApp 工厂函数
import { createApp } from 'vue'
#导入App.vue
import App from './App.vue'

//导入路由
import router from './router'

//拆解工厂函数创建对象
const app = createApp(App)
//使用路由
app.use(router)
//挂载在#app的div上
app.mount('#app')

Composition API VS Option API

  • vue3这个版本,而且写法发生了巨大的变化。 肯定是vue2原有的写法有一些问题,退出vue3

  • vue2的写法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // 导出一个对象
    export default {
    // 我要数据
    data: function() {
    return {}
    },
    methods: {
    a: function() {}
    },
    computed: {},
    watch
    }

    编码风格就做Option API(配置型api)

    优点: 清晰,简单

    缺点:this太多(this是动态), 因为this是动态的,所以只有在代码执行的时候才知道this指向哪一个,数据来源不太清晰,类型就不好推断,不适合和ts配置。 不太适合开发大型项目

  • 配置项 Option API 数据,方法,计算属性等等 写在规定的配置项里面

  • Composition API 组合式API
  • 我们可以更加优雅的组织我们的代码,函数。让相关功能的代码更加有序的组织在一起

  • 配置项API vs 组合式API

1 setup 函数 【重点】

  • 新的 option, 所有的组合 API 函数都在此使用, 只在初始化时执行一次(理解为一个生命周期,比created更早,没有this)
  • 函数如果返回对象, 对象中的属性或方法, 模板中可以直接使用
1
`setup` 中你应该避免使用 `this`,因为它不会找到组件实例。`setup` 的调用发生在 `data` property、`computed` property 或 `methods` 被解析之前,所以它们无法在 `setup` 中被获取,这也是为了避免setup()和其他选项式API混淆。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
<template>
{{msg}}
<button @click='sayHi'>点击我</button>
</template>

//基本用法
<script>
export default {
setup(){
#setup 函数里面执行的代码相当于 created生命周期

const msg = '小樱'
const sayHi = () => {
return 'hi'
}
#return的属性和方法 可以在模板里面直接用
return {
msg,
sayHi
}
}
}
</script>


#高级用法 推荐!!!
<template>
{{msg}}
<button @click='sayHi'>点击我</button>
</template>

#setup 表示script里面的代码全部都写在setup函数内部 而vue3代码全部可以在里面跑完
#定义在全局的 数据和方法 在模板中使用会自动解包
<script setup>
#不需要return 定义好直接可以在模板里面使用 非常的nice!
const msg = '小樱'
const sayHi = () => {
return 'hi'
}
</script>

2 ref shallowRef triggerRef 数据响应 【重点】

作用:

  • 通常用来把一个基本类型变成响应式数据

    • 把基本类型变成对象: { value: 值 }
    • 重写了上一步对象下的value的getter/setter
      • Object.defineproperty
    • 改数据: 对象.value = 新值(触发setter,页面更新)
  • 获取dom节点

  • 获取组件实例

  • 你也可以给ref传递一个对象也是可以的

    1
    2
    3
    4
    5
    6
    7
    8
    const user = ref({
    name: 'jgmiu',
    age: 20
    })

    const changeUserName = () => {
    user.value.name = '张三'
    }

ref和ts配合使用:

  • 定义一个ref变量(value是字符串)

    1
    2
    3
    4
    5
    // 因为我们给name赋值了一个‘张三’, 那么name.value的类型永远就是string
    const name = ref('张三')

    // 显式的申明name下的value就是string类型
    const name = ref<string>('张三')
  • 定义一个ref变量(value是一个对象)

    • 先定义一个接口或者type来描述对象
    • 传给ref<定义好的接口或者type>
  • 基本数据类型绑定
    • 创建 RefImpl 响应式 Proxy 对象 ref 内部: 通过给 value 属性添加 getter/setter 来实现对数据的劫持
    • 定义数据之后,模板里面直接使用值
    • 修改数据 用.value 属性修改
  • 响应式状态需要明确使用响应式 APIs 来创建。和从 setup() 函数中返回值一样,ref 值在模板中使用的时候会自动解包
  • shallowRef 创建引用类型ref 不推荐直接使用ref(引用类型) 赋值得重新赋值
  • triggerRef 如果通过shallowRef 定义引用类型数据要改变某个属性不会更新视图,必须调用triggerRef 收集数据依赖重新渲染视图
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<template>
{{变量名}}
<button @click='change'>点击我</button>
</template>

<script setup lang='ts'>
#引入ref 函数 Ref类型别名
import { ref,Ref,shallowRef } from 'vue'

#创建响应式数据
const 变量名:Ref<类型> = ref(初始值)
//or const 变量名 = ref<类型>(值类型)

#修改响应式数据 注意别修改原数据
const change = ()=> {
变量名.value = 修改之后的值
}

#如果是引用类型要定义ref 推荐shallowRef 只能做全赋值 不能去改深层次
const tableData = shallowRef([])

const changeTable = ()=>{
tableData.value = [1,2,3]
}


#如果要单独修改深层次属性 需要配合triggerRef 让数据改变被监听修改视图
const formData = shallowRef({
name:'哈哈'
})

const changeForm = ()=>{
formData.value.name = '嘿嘿'
#triggerRef(shallowRef定义的值)
triggerRef(formData)
}


</script>
  • dom 绑定
1
2
3
4
5
6
7
8
9
10
11
12
<template>
<标签 ref='变量名'></标签>
</template>

<script setup>
#引入ref 函数
import { ref } from 'vue'

#创建dom 绑定必须变量名相同 注意必须初始值为null 因为模板还未渲染
const 变量名 = ref<null | HTMLXXXElement>(null)

</script>
  • 示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
<template>
<div>
<h1>ref</h1>
<p>{{ num1 }}</p>
<p>{{ num2 }}</p>
<button @click="num2++">num2++</button>

<hr />
<h3>ref绑定dom</h3>
<input type="text" ref="iptRef" value="222" />
<hr />
<h3>shallowRef</h3>
<p>{{ person }}</p>
<button @click="changeName">修改name</button>
</div>
</template>

<script setup lang="ts">
import {ref, Ref, shallowRef, triggerRef} from 'vue'
//方式1 引入Ref 进行约束 不推荐
const num1: Ref<number> = ref(10)

//方式2 函数泛型约束 推荐!
const num2 = ref<number>(20)

//ref获取dom
const iptRef = ref<null | HTMLInputElement>(null)
setTimeout(() => {
// ?. 是短路运算符缩写 iptRef.value && iptRef.value.value
console.log(iptRef.value?.value)
}, 1000)

interface Person {
name: string
age: number
}
//shallowRef
const person = shallowRef<Person>()
person.value = {
name: '小橘猫',
age: 11,
}
const changeName = () => {
//重新赋值
// person.value = {
// name: '大花猫',
// age: 12,
// }

//修改shallowRef某个属性并不会触发 响应式
person.value!.name = '大花猫'
//triggerRef 重新触发响应式
triggerRef(person)
console.log('person', person)
}
</script>

3 reactive shallowReactive readonly 数据响应 【重点】

作用: **引用类型的数据响应定义 **

  • 作用: 定义多个数据的响应式
  • const proxy = reactive(obj): 接收一个普通对象然后返回该普通对象的响应式代理器对象
  • 响应式转换是“深层的”:会影响对象内部所有嵌套的属性
  • 内部基于 ES6 的 Proxy 实现,通过代理对象操作源对象内部数据都是响应式的
  • 语法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<template>
{{state.属性1}}
</template>

<script setup>
#引入reactive 函数
import { reactive } from 'vue'

#创建响应式数据
const state = reactive({
属性1:值1,
})

#数组赋值
#方式1 push
const tableData = reactive<Array<number>>([])
const getData1 = () => {
//解构+push
tableData.push(...[1, 2, 3, 4])
}

#方式2 把数组作为属性赋值
const state = reactive<{list: Array<number | undefined>}>({
list: [],
})
const getData2 = () => {
state.list = [2, 2, 3, 4]
}

</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<template>
{{state.name}}
{{state.k1.k2}}
<button @click='change'>点击我</button>
</template>

<script setup>
#引入reactive 函数
import { reactive } from 'vue'

#创建响应式数据
const state = reactive<类型>({
name:'小樱',
k1:{
k2:666
}
})

#修改响应式数据 注意别修改原数据
const change = ()=>{
state.name = '小狼'
state.k1.k2 = 999
}
</script>

4 toRefs 【重点】

作用:将响应式对象转换为普通对象,其中结果对象的每个 property 都是指向原始对象相应 property 的 ref

当从组合式函数返回响应式对象时, toRefs 非常有用,这样消费组件就可以在不丢失响应性的情况下对返回的对象进行解构/展开:

  • 语法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<script setup>
#引入reactive toRefs 函数
import { reactive, toRefs } from 'vue'

#创建响应式数据

const state = reactive<类型>({
属性1:值1,
属性2:值2
})

#数据解构出来,创建属性的ref,都通过value来获取值
const { 属性1, 属性2 } = toRefs(state)
</script>

5 toRef unref toRaw

  • toRef可以用来为源响应式对象上的某个 property 新创建一个 ref 。然后,ref 可以被传递,它会保持对其源 property 的响应式连接。

  • unRef 还原ref定义的响应式数据

  • toRaw 还原reactive定义的引用类型数据

  • 语法

1
2
3
4
5
6
7
8
9
10
11
12
13
#toRef
const state = reactive({
属性1:值1
})

const ref变量 = toRef(state, '属性1')

#unref 还原ref
const msg = ref<string> = '你好啊'
const unMsg = unref(msg) //你好啊

#toRaw 还原reactive
const stateRaw = toRaw(state) //{属性1:值1}

6 计算属性

  • computed函数:
    • 与computed配置功能一致
    • 只有getter
    • 有getter和setter
  • 语法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#简单用法 只有getter形式
const 计算属性变量 = computed<返回值类型>(() => {
return 处理好的数据
});

#有getter 有 setter形式
const 计算属性变量 = computed<返回值类型>({
get() {
return xx
},

set(value) {

},
});

7 侦听器 watch

  • watch函数

    • 与watch配置功能一致

    • 监视指定的一个或多个响应式数据, 一旦数据变化, 就自动执行监视回调

    • 默认初始时不执行回调, 但可以通过配置immediate为true, 来指定初始时立即执行第一次

    • 通过配置deep为true, 来指定深度监视

  • watchEffect函数 【理解】

    • 不用直接指定要监视的数据, 回调函数中使用的哪些响应式数据就监视哪些响应式数据
    • 默认初始时就会执行第一次, 从而可以收集需要监视的数据
    • 监视数据发生变化时回调
  • 语法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#简单用法 
watch(ref 或者reactive 对象数据,(newV,oldV)=>{
//观察数据变化执行 里面代码
},{
immediate:true, //立即执行
deep:true //深度监听
flush?: 'pre' | 'post' | 'sync' // 默认:'pre' post渲染之后执行
})

#观察多个数据变化
watch([数据1,数据2],()=>{

})

#注意:如果观察是reactive内部某个属性变化 需要箭头函数返回观察的属性
watch(()=>reactive对象.属性,()=>{

})

#watchEffect 会立即执行里面代码 监视所有回调中使用的数据
const stop = watchEffect((onCleanup)=>{
#形参onCleanup函数 观察变化之前执行回调函数 用于清除副作用
onCleanup(()=>{

})
#观察数据变化 执行里面副作用代码
})

#stop 停止观察的回调

8 生命周期

因为 setup 是围绕 beforeCreatecreated 生命周期钩子运行的,所以不需要显式地定义它们。换句话说,在这些钩子中编写的任何代码都应该直接在 setup 函数中编写。

下表包含如何在 setup () 内部调用生命周期钩子:

选项式 API Hook inside setup
beforeCreate Not needed* 不需要
created Not needed* 不需要
beforeMount onBeforeMount 挂载之前
mounted onMounted 页面加载完成时执行 ###
beforeUpdate onBeforeUpdate
updated onUpdated
beforeUnmount onBeforeUnmount
unmounted onUnmounted 页面销毁时执行 ###
errorCaptured onErrorCaptured
renderTracked onRenderTracked
renderTriggered onRenderTriggered
activated onActivated
deactivated onDeactivated
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<script setup >
import { onMounted, onActivated, onUnmounted, onUpdated, onDeactivated } from 'vue';

onMounted(() => {
console.log("组件挂载")
})

onUnmounted(() => {
console.log("组件卸载")
})

onUpdated(() => {
console.log("组件更新")
})
onActivated(() => {
console.log("keepAlive 组件 激活")
})

onDeactivated(() => {
console.log("keepAlive 组件 非激活")
})
</script>

9 defineProps 和 defineEmits 父子传参

注意:definePropsdefineEmits 都是只在 <script setup> 中才能使用的编译器宏

1
2
3
4
5
6
为了声明 `props``emits` 选项且具备完整的类型推断,可以使用 `defineProps``defineEmits` API,它们在 `<script setup>` 中都是自动可用的:

- **`defineProps``defineEmits` 都是只在 `<script setup>` 中才能使用的****编译器宏**。他们不需要导入,且会在处理 `<script setup>` 的时候被编译处理掉。
- `defineProps` 接收与 `props` 选项相同的值,`defineEmits` 也接收 `emits` 选项相同的值。
- `defineProps``defineEmits` 在选项传入后,会提供恰当的类型推断。
- 传入到 `defineProps``defineEmits` 的选项会从 setup 中提升到模块的范围。因此,传入的选项不能引用在 setup 范围中声明的局部变量。这样做会引起编译错误。但是,它*可以*引用导入的绑定,因为它们也在模块范围内。

父传子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#父组件
<template>
<子组件 自定义属性1='静态' :自定义属性2='动态值'></子组件>
</template>

#子组件
<template>
{{自定义属性1}}
{{自定义属性1}}
</template>
<script setup lang='ts'>
#定义类型 or 使用接口
type Props = {
自定义属性1: 类型1
自定义属性2: 类型2
}
#接受子组件传递的参数
const props = defineProps<Props>()

#or 定义默认值
const props = withDefaults(defineProps<Props>(), {
自定义属性1: '默认字符串',
自定义属性2: 10,
自定义属性3: () => [2, 2, 2], #数组 对象必须用箭头函数返回
自定义属性4: () => ({对象}),
})
</script>

子传父

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#子组件
<script setup>
#定义子组件的 自定义事件
//定义事件
const $emits = defineEmits<{
(e: '自定义事件1',参数: 类型): void
(e: '自定义事件2'): void
}>()

#触发子传父事件
$emits('自定义事件1', 数据)
</script>

#父组件
<template>
<子组件 @自定义事件1="处理函数"></子组件>
</template>

<script setup>

#子组件 触发自定义事件1 父组件的处理函数 收到子传父的数据
const 处理函数 = (data) => {

}
</script>

10 defineExpose 子组件暴露数据方法

使用 <script setup> 的组件是默认关闭的,也即通过模板 ref 或者 $parent 链获取到的组件的公开实例,不会暴露任何在 <script setup> 中声明的绑定。

为了在 <script setup> 组件中明确要暴露出去的属性,使用 defineExpose 编译器宏:

  • 语法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#子组件
<script setup>
type MyExpose = {
属性1:类型1
方法1():void
}
#子组件暴露
defineExpose<MyExpose>({
属性1:值1,
方法1() {}
})

</script>

#父组件
<template>
<子组件 ref='子组件ref'></子组件>
</template>

<script setup>
import 子组件 from './地址'
import { onMounted, ref } from 'vue';

const 子组件ref = ref<InstanceType<typeof 子组件> | null>(null)
#注意 在生命周期onMounted后 才能接受子组件暴露的数据和方法
onMounted(() => {
子组件ref.value.方法1();
子组件ref.value.属性1
})

</script>

11 自定义hooks函数

  • 使用Vue3的组合API封装的可复用的功能函数
  • 用来取代vue2 mixin 目的也是抽离公共js逻辑
  • 命名 userXxx开头的函数 一般会把一个功能封装在一个js中

例如 封装一个table宽度动态变化功能的hooks函数

  • hooks/useTableWidth.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
/* table 宽度动态变化 */
import { onMounted, onUnmounted, ref } from 'vue'

/* 表单宽度设置 */
const useTableWidth = () => {
//dom
const tableRef = ref(null)

const w = ref('')

//计算宽度
const calcTableWidth = () => {
// 这个不能自动修改 有点怪
// table.value.style.width = document.body.clientWidth - 140 + 'px'
w.value = document.body.clientWidth - 140 + 'px'
}

//挂载后
onMounted(() => {
//初始化计算一次
calcTableWidth()
//窗口宽度变化计算一次
window.addEventListener('resize', calcTableWidth)
})

//销毁后
onUnmounted(() => {
window.removeEventListener('resize', calcTableWidth)
})

return {
tableRef,
w,
}
}

export default useTableWidth
  • 使用
1
2
3
4
5
6
7
8
9
10
11
12
13
<template>
<!-- 表格 -->
<el-table ref="tableRef" :style="{ width: w }" ></el-table>
</template>

<script setup>

import useTableWidth from '@/hooks/useTableWidth';
//计算table宽度

const { tableRef, w } = useTableWidth()
</script>

12 其他新组件

Fragment(片断)

  • 在Vue2中: 组件必须有一个根标签
  • 在Vue3中: 组件可以没有根标签, 内部会将多个标签包含在一个Fragment虚拟元素中
  • 好处: 减少标签层级, 减小内存占用
1
2
3
4
5
#其实 两个div被包在Fragment 虚拟元素中 感觉没有根元素
<template>
<div></div>
<div></div>
</template>

Teleport(传送门)

  • Teleport 提供了一种干净的方法, 让组件的html在父组件界面外的特定标签
    • 例如:可以把一个组件里面的弹窗popUp 直接插进body标签里面
1
2
3
4
5
6
#相当于 body.appendChild(弹窗)
<teleport to="body">
<div class="pop">
弹窗
</div>
</teleport>

作业:

  • 语法跟着文档,过两边

  • 渲染商品列表(vue3.2 + ts)

    • 尝试使用ref
    • 尝试使用reactive

0.0 课程介绍

  • Vue介绍 【了解】
  • Vue项目的搭建 【掌握】
  • 项目目录详解【掌握】
  • Vue组件化【掌握】
  • Vue的表达式 【重点】
  • Vue的指令【重点】

1.0 什么是Vue?【了解】

Vue是一个构建用户界面(UI)的JS库。

1.1Vue的特点

  • 小 20k 【大概11000多行】
  • 性能强 虚拟dom
  • 双向绑定
  • 生态丰富

1.2 Vue历史 [了解]

1.0 -> 2015.10

2.0 -> 2016.10 https://v2.cn.vuejs.org/v2/guide/installation.html

3.0 -> 2020.09 https://cn.vuejs.org/

2023年5月20日 :3.3.4

2.0 Vue的环境搭建【掌握】

2.1 CDN使用

  • 直接使用网络资源
  • 将vuejs下载到本地

2.2 脚手架安装

先安装Vue脚手架

1
2
3
4
5
6
vue --version // 查看脚手架的版本

安装:
npm i @vue/cli -g

yarn add global @vue/cli

#注意 如果你的yarn无法使用,可能是没有安装yarn npm i yarn -g,或者是没有配置环境变量

yarn global bin 找到你的yarn安装位置

我的电脑-> 右键属性 -> 高级系统配置 ->环境变量 -> path、

2.3 使用脚手架创建Vue的项目

1
vue create 项目名      // vue-demo    demo

一顿操作

见图形笔记

#如果你想删除你保存预设方案,去我的电脑->用户 ->你的电脑名 ->删掉 vuerc

C:\Users\你的电脑名

3.0 组件化【掌握】

3.1 什么是组件化?

相当于将一个大的页面拆分成多个大组件,通过小组件组合成大组件,这个用组件拼接成页面的思维就叫组件化

特点:

  • 可复用
  • 易维护
  • 可组合

3.2 单文件组件

一个.vue文件就是一个单文件组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<template> // 你的html
<div>
不仅可以写样式,还可以写很多vue提供的语法及指令
...
</div>
</template>


<script> // 你的js
// 你在此处暴露了什么出去? VueComponent类
export default {
// 你的VUE配置选项
data(){ // 专门放你的数据
return {
xx
}
},

methods:{ // 专门放你的方法们
fn(){ },
foo(){ },
}

}

</script>

<style lang="less" scoped> // 写你的css

</style>

注意: 推荐插件

vuter: 语法提示

Vue VSCode Snippets : 快捷键

path: 路径提示

4.0 Mustache胡须表达式【重点】

1
2
3
4
5
6
7
8
9
10
11
{{ 表达式 }}
`${ 表达式 }`

看是不是表达式?
100 // 是
let arr = [10,20,30]
arr.map(v=>v+100).join('-') // 是
true?'不是''是' // 是
if(true){ a = 10 } // 不是
for(let i=0;i<10;i++){ a+=i } // 不是
arr.forEach(i=>{ i+10 }) // 不是

什么是表达式

1、能够得到唯一结果的一句运算

2、可以放在赋值表达式右侧

5.0 指令 【重点】

vue的指令是什么?

帮助我们操作dom,提高效率

vue的指令就是一个以v-开头的自定义属性

1
<div id="dxx" class="xxx" age="10" v-xx="表达式">

5.1 v-text 和 v-html

作用:帮助我们渲染dom节点

v-text: 底层是 textContent ,渲染数据,不能识别标签

v-html: 底层是 innerHTML ,渲染数据, 能够识别标签

5.2 v-if 和 v-show

作用: 帮助我们根据条件渲染页面节点

v-show: 底层是通过控制display的属性来进行显示隐藏

v-if: 底层是通过删除页面或重新渲染节点来实现显示隐藏

5.3 v-if 和 v-else-if 和 v-else

作用: 帮助我们根据条件渲染页面节点,跟我们js中的if…else if…else是一样的用法,如果从上到下开始判断,当满足条件就不渲染后面的内容

5.4 v-for 循环

作用 : 帮助我们循环渲染页面

1
2
3
4
5
6
7
8
9
10
11
12
13
// 数组
let arr = [1,2,3]

<div v-for="(item,index) in arr"> // item 是你的每一项 index是你的索引
{{ item }} -- {{ index }}
</div>


// 对象
let obj = {name:'张无忌',age:18,like:'金花宝宝'}
<div v-for="(value,key,index) in obj">
{{ value }} -- {{ key }} -- {{index}}
</div>

5.5 v-model

作用:实现数据的双向绑定,只用使用v-model 与data里的数据就不分彼此 表单之王

1
2
<input v-model="data的数据" />

5.6 v-on 事件

作用:绑定事件, 简写为@

1
2
3
4
5
6
7
8
9
常规写法:
<button v-on:事件类型=“表达式”>xxx</button>
<button v-on:事件类型=“处理函数”>xxx</button>
<button v-on:事件类型=“处理函数(参数1,参数2,...)”>xxx</button>

简写:
<button @事件类型=“表达式”>xxx</button>
<button @事件类型=“处理函数”>xxx</button>
<button @事件类型=“处理函数(参数1,参数2,...)”>xxx</button>

0.0 自定义组件的使用 【掌握】

  • 引入组件
1
2
import 组件名 from '路径/文件名'

  • 注册组件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<script>
export default {
components:{ // 组件注册
组件名:组件名,
组件名1
},
data(){ // 数据
return {}
},
methods:{ // 方法

}

}

</script>


  • 使用组件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    <template>
    <div>
    <组件名></组件名>
    <组件名 />

    <!-- 使用组件 -->
    <CommonHeader></CommonHeader>
    <CommonContent />
    <CommonFooter></CommonFooter>

    <!-- 第二用法 推荐-->
    <common-header></common-header>
    <common-content />
    <common-footer />
    </div>
    </template>


1.0 课程介绍

  • v-bind 动态绑定属性 【重点】
  • 其他指令 【了解】
  • 计算属性computed 【重点】
  • 过滤器 filters 【掌握】
  • 侦听器 watch 【掌握】
  • Vue生命周期 【掌握】

2.0 v-bind 【重点】

作用:帮助我们操作节点属性,让属性变成数据驱动

1
2
3
4
5
6
7
8
9
<标签 v-bind:class='表达式'  v-bind:style="表达式" v-bind:src="表达式">
</标签>

简写:

<标签 :class='表达式' :style="表达式" :src="表达式">
</标签>


3.0 其他指令 【了解】

3.1 v-pre

作用:不编译胡须表达式

3.2 v-once

作用:只渲染一次,当他所依赖的数据发生改变时,也不会更新渲染

3.3 v-cloak

作用:隐藏胡须表达式,直到有数据时才渲染,只有直接在项目里引入vuejs才会出现,当前主流脚手架版本没有这个问题

4.0 computed 计算属性 【重点】

用于一堆逻辑计算,返回一个唯一的结果 ,与methods定义方式一样,计算属性直接使用他的函数名(不需要加小括号),就等于他的返回结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<template>
<div>
{{ 函数名 }}
</div>
</template>
<script>
export default {
....
computed:{
函数名(){
一堆的逻辑运算

return '最终的结果'
}
}
}
</script>


计算属性的特点:

  • 计算属性用于一堆逻辑运算,直接使用函数名就等于使用了最终结果
  • 计算属性依赖的数据发生改变,他就会重新计算
  • 计算属性存在依赖缓存,性能强,如果依赖的数据更新就会重新计算,如果不更新就直接返回上一次的计算结果

computed计算属性 与 methods方法的区别 【面试题】

  • computed计算属性必须有返回值,methods不一定有
  • 调用方式不一样,computed计算属性直接使用函数名,methods方法需要函数名()调用
  • computed计算属性有依赖缓存,methods方法每次调用都会重新计算结果,计算属性如果数据没有改变,他就将上一次缓存的结果直接返回,就不重新计算了

5.0 filters 过滤器【掌握】

作用:用于处理页面数据的显示格式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<template>
<div>

{{ msg | 过滤器函数名 }}
</div>
</template>
<script>
export default {
data(){
return {
mes:'xxx'
}
},
methods:{},
components:{},
computed:{},
filters:{
过滤器函数名(参数){
一堆参数的处理
return '处理完的结果'
}

}


}

</script>


6.0 侦听器 watch 【掌握】

作用:用于监听数据的变化,进行相应函数的操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<template>
<div>

{{ msg | 过滤器函数名 }}
</div>
</template>
<script>
export default {
data(){
return {
msg:''
}
}
watch:{
需要监听的数据(newVal,oldVal){
// 一堆的后续操作
}
},


}

</script>




7.0 生命周期 【重点】

什么是生命周期?

是指组件从创建到渲染到更新到销毁的周期过程

vue的声明周期分为4大阶段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#注意:他们都是内置选项
// 创建前后
beforeCreate(){}, // 创建前 组件实例对象还没有创建,data里的数据不能访问
created(){}, // 创建后 ******* 组件实例已经创建完毕,data里的数据可以访问,此时页面dom还没挂载
#注: created 一般用于初始化页面数据发送ajax
// 挂载前后
beforeMount(){} // 挂载前 生成虚拟dom,页面dom还没有挂载
mounted(){} // 挂载后 ******* 页面dom已经挂载完毕,可以操作dom
#注: mounted 一般用于操作dom节点
// 更新前后
beforeUpdate(){} // 更新前
updated(){} // 更新后
// 销毁前后
beforeDestroy(){} // 销毁前 ** 一般用于清除绑定事件、定时器
destroyed(){} // 销毁

总结:vue的生命周期的作用,在组件各个阶段执行相应的生命周期钩子函数,给用户添加自己代码的机会

作业:

1 购物车案例

2 交互代码敲3遍

3 面试题: 计算属性computed和方法methods有什么区别?

4 选做题:操作题

5 手写配置选项 和 指令

0.0 课程介绍

  • 组件通信【重点】
  • slot 插槽【掌握】
  • 封装组件【掌握】
  • UI组件库【掌握】

1.0 组件通信

组件为了实现交互,通过数据传递实现,这个就叫做组件通信

自定义组件的使用:

  • 引入组件
  • 注册组件
  • 使用组件

1.1组件通信类型:

  • 父传子【重点】: 在父页面的子组件标签上定义需要传递参数,在子组件页面通过配置选项props 进行接收参数,使用方法与data里的数据一样
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
<template>
<son 参数1=“表达式” :参数2=“msg12”></son>
</template>
<script>
import Son from 'xx/Son.vue' // 引入组件

export default{
// 注册组件
components:{ Son },
data(){
return {
msg:'xxx'
}
}
}
</script>


# 子组件接收参数
<template>
<div>
{{ msg }}
{{ 参数1 }}
</div>
</template>
<script>
// 子组件接收父组件传入的参数
export default{
// 数组接收方法1
props:['参数1','参数2']

// 对象接收方法2 【推荐】
props:{
参数1:{ // 配置选项
required:true, // 是否必传
type:String|Number|Boolean|Object|Array, // 数据类型:构造函数
default:''|0|false
// 注意:如果类型为引用类型 ()=>{return {}}、()=>{return []}
}
},
data(){
return {
msg:’xxx‘
}
}
}
</script>


  • 子传父【重点】:vue是单向数据流,子组件不可以直接修改父组件传入的数据,只能通过触发事件进行数据修改;

    发送:通过this.$emit(‘自定义事件名‘,参数),将参数传递给父组件;

    接收:父组件页面在子组件的标签上绑定自定义事件名接收传入参数;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
# son.vue
<template>
<div>
...内容
<button @click="处理函数"> 修改 </button>

</div>
</template>
<script>
export default{
...
// 方法
methods:{
处理函数(){
// 通过$emit触发自定义事件,携带对应参数给父组件
this.$emit('自定义事件名',参数1,参数2...)
}
}
}

</script>


#父组件
<template>
<son 参数1=“表达式” :参数2=“msg12” @自定义事件名="处理函数"></son>
</template>
<script>
import Son from 'xx/Son.vue' // 引入组件

export default{
// 注册组件
components:{ Son },
data(){
return {
msg:'xxx'
}
},
// 方法
methods:{
处理函数(参数1,参数2){
// 处理逻辑后修改数据
this.msg = 参数1
}
}
}
</script>

  • 乱传(中央事件总线bus)【理解】: 找一个Vue实例当中介,通过这个中介进行数据的传递和接收

    特点: 用起来非常简单,缺点就是乱,维护成本极高

1
2
3
4
5
6
7
8
9
1、创建中介 -- 创建vue的实例 挂载到Vue的原型上
Vue.prototype.$bus = new Vue()
2、传递信息
this.$bus.$emit('自定义事件名',参数)
3、接收信息
created(){
this.$bus.$on('自定义事件名',(参数)=>{ // 处理数据 })
}

父子关系确定: 如果你在我的页面引入,注册,使用,那么我就是你爸爸

  • 依赖注入【不讲】:祖先组件注入某个属性,在子孙组件依赖这个变量
  • vuex 状态管理库
  • 插槽 :他传递元素
  • 路由传参: Router

2.0 插槽的基本使用 【掌握】

在子组件里挖坑,在父组件使用时填坑就可以了,这就是插槽的作用

2.1 插槽的类型

  • 匿名插槽
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#子组件 son.vue
<template>
<div>
<slot></slot>
</div>
</template>

#父组件 father.vue
<template>
<div>
<son>
写入你想写内容包括元素标签
</son>
</div>
</template>

  • 具名插槽
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#子组件 son.vue
<template>
<div>
<slot name='插槽名1'></slot>
<slot name='插槽名2'></slot>
</div>
</template>

#父组件 father.vue
<template>
<div>
<son>
<写入你想写内容包括元素标签 slot="插槽名2" />
<写入你想写内容包括元素标签 slot="插槽名1" />
</son>
</div>
</template>

  • 作用域插槽 (相当于将子组件的数组 传给父组件)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#子组件 son.vue
<template>
<div>
<slot name='插槽名1' :需要传出去的属性1=“属性值1” :需要传出去的属性2=“属性值2” ></slot>
<slot :需要传出去的属性1=“属性值1” :需要传出去的属性2=“属性值2”></slot>
</div>
</template>

#父组件 father.vue
<template>
<div>
<son>
# 具名插槽
<写入你想写内容包括元素标签 slot="插槽名1" slot-scope="scoped">
{{ scoped }}
</写入你想写内容包括元素标签>
# 匿名插槽
<写入你想写内容包括元素标签 slot-scope="scoped" >
{{ scoped }}
</写入你想写内容包括元素标签>

</son>
</div>
</template>

3.0 组件库的使用

去相应的官网找到合适的组件, C + V

  • 根据你选择的元素,查看对应的属性、方法、事件文档

ElementUI (https://element.eleme.cn/#/zh-CN)

1
2
3
4
5
6
7
8
9
10
11
1、下载
npm i element-ui -S

yarn add element-ui -S

2、引入及使用(在main.js引入)
import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';

Vue.use(ElementUI);

Vant UI (https://vant-contrib.gitee.io/vant/v2/#/zh-CN/)

1
2
3
4
5
6
7
8
9
10
11
1、下载
npm i vant@latest-v2 -S

yarn add vant@latest-v2 -S

2、引入及使用(在main.js引入)
import Vant from 'vant'; // js
import 'vant/lib/index.css'; // css

Vue.use(Vant);

0.0 课程介绍

  • Vue 路由库Router 【重点】

    • 安装
    • 基本使用
    • 路由配置
    • 路由模式
    • 路由传递参数
    • 路由内置对象
    • 路由守卫
  • Vue的内置API 【掌握】

    • ref

    • Vue.set

    • Vue.nextTick

    • Vue.filter

    • Vue.component

    • Vue.use

    • Vue.directive

1.0 Vue的路由Router 【重点】

1.1 路由作用

进行页面的跳转(相当于a标签),Vue是SPA单页面应用,他的页面跳转必须使用Vue-Router路由进行实现

1.2 路由的安装

vue create 项目名 创建一个带有Vue路由的项目

1.3 路由的使用

一级路由配置

  • 1 建(建大页面)
  • 2 配 (配置路由选项,一一对应)
  • 3 给出口及测试
1
2
3
4
5
6
7
8
9
10
11
12
13
14
如果你的页面需要进行路由显示,必须给出口 <router-view></router-view>,一级路由出口在App.vue页面,嵌套路由出口在父页面
路由出口:就是你页面需要渲染的位置
测试:在浏览器路径输入对应path


// 配置路由列表
const routes = [
{ path: '/', redirect: '/discover' }, // redirect: 重定向指定的路由(一级路由)
// 配置一级路由
{ path: '/discover', component: DiscoverView },
{ path: '/my', component: MyView },
{ path: '/friend', component: FriendView },
]

  • 4 配置导航
1
2
3
4
<router-link to="/discover">发现音乐</router-link>
<router-link to="/my">我的音乐</router-link>
<router-link to="/friend">关注音乐</router-link>

嵌套路由配置

  • 1 建 (建大页面)
  • 2 配 (配置路由选项,一一对应)
  • 3 给出口及测试
1
2
3
4
5
6
7
8
9
10
11
12
13
嵌套路由的出口在父页面 <router-view></router-view>

{
path: '/discover', component: DiscoverView,
redirect: '/discover/toplist', // redirect: 重定向指定的路由(嵌套路由)
// 配置嵌套路由
children: [
{ path: '/discover/recommend', component: RecommendView },
{ path: '/discover/toplist', component: ToplistView },
{ path: '/discover/playlist', component: PlaylistView },
]
},

  • 4 配置导航
1
2
3
4
5
6
7
8
9
<div class="discover">
<!-- 嵌套路由的出口在父页面 -->
<router-link to="/discover/recommend">推荐</router-link>
<router-link to="/discover/toplist">排行榜</router-link>
<router-link to="/discover/playlist">歌单</router-link>
<!-- 嵌套路由的出口 -->
<router-view></router-view>
</div>

1.4 路由的模式 【重点】

【面试题】

  • hash模式 :地址栏带#, 底层实现的是用 onhashchange的一个方法

  • history模式 : 地址栏不带#,底层实现是用的h5的 pushState 方法

    区别:

    [1] : 地址栏一个带#,一个不带#

    [2] : 底层实现的原理不一样

    [3] : hash模式根history模式在开发中没有任何区别,但是在打包后的代码hash模式没有问题,history模式会存在刷新后页面丢失情况

    #解决办法: 只能让后端或者运维,对nginx代理服务器进行相应重定向的配置

1.5 路由的传参 【掌握】

  • query传参
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 路由提供了一个跳转的方法 this.$router.push('/路径')
// query配置项
1、路由跳转
this.$router.push({
path:'/路径',
query:{
键名:键值,
键名1:键值1,
}
})
2、获取参数
this.$route.query

特点:
1、页面刷新参数依旧存储
2、不能直接传递引用类型(可以用JSON.stringify 转成字符串【不推荐】)

  • params传参
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 路由提供了一个跳转的方法 this.$router.push('/路径')
// params配置项
1、路由跳转
this.$router.push({
name:'路由名',
params:{// 并且可以携带引用类型
键名:键值,
键名1:键值1,
}
})
2、获取参数
this.$route.params

特点:
1、页面可以携带引用类型
2、刷新页面数据丢失(将刷新按钮禁用或者去除)

  • 动态路径传参
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 路由提供了一个跳转的方法 this.$router.push('/路径')
1、路由配置项中进行路径动态传参配置
{ path: '/my/:变量名', component: MyView },
2、路由跳转
this.$router.push({
path:'/my/传入的值'
})
3、获取参数
this.$route.params

特点:
1、刷新页面后不会丢失数据
2、动态路径必须携带参数

1.6 路由的两个内置对象【掌握】

  • $router

路由实例对象,他主要提供一些页面跳转的方法(他其实就等 === VueRouter)

​ push

​ go

replace

  • $route

路由信息对象,他主要提供当前页面的参数信息

params

query

path

1.7 路由守卫【理解】

全局前置路由守卫,监听路由变换,判断是有权限

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
router.beforeEach((to,from,next)=>{
// to 你要去往哪里
// from 你从哪里来
// next 是个函数,如果直接调用就可以前往,如果传入路径,就前往指定页面
})

router.beforeEach((to, from, next) => {
// 如果我跳转目标是我的,那我就让你重定向关注
if (to.path == '/discover/recommend') {
next({ path: '/discover/playlist' })
} else {
next()
}

// const token = localStorage.getItem('token123')
// if (!token) {
// next({ path: '/login' })
// } else {
// next()
// }
})

2.0 Vue的内置API【掌握】

2.1 ref

作用:用于获取Dom节点,相当于元素选择器,如果你获取的是子组件,相当于获取到自组件的实例对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<template>
<div>
#dom
<span ref="ref的值1">dom节点</span>

#子组件
<son ref="ref的值2"></son>
</div>
</template>

<script>
export default {
methods:{
init(){
this.$refs.ref的值1 #DOM节点
this.$refs.ref的值2 #子组件的实例对象
}
}
}
</script>
定义ref的值: 在父页面的子组件(或dom)标签上定义属性ref=“ref的值”
获取ref的值: 在js中通过this.$refs.ref的值 来获取子组件实例(或dom节点)


2.2 Vue.set

他可以帮助我们重新挟持【绑架】数据,让数据具备响应式

理论【面试题】:因为Vue底层会对data里进行挟持,当初始状态对象没有这个属性,后期添加的属性没有被挟持,不具备响应式,通过Vue.set方法让数据重新挟持

1
2
3
4
5
6
import Vue from 'vue'
Vue.set(需要挟持的对象, "属性", "修改的值");

this.$set(需要挟持的对象, "属性", "修改的值")


数组怎么改?

解决方法:数组的变更方法,这是被Vue重写的方法,可以让数组里的数据修改时也具备响应式

1
2
3
4
5
6
7
8
9
10
11
push()
pop()
shift()
unshift()
splice()
sort()
reverse()

以上方法已经升级啦


2.3 Vue.nextTick

他是一个回调函数,帮你解决异步的问题,在下一次页面节点更新完毕后触发

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import Vue from 'vue'
Vue.nextTick(()=>{
// 下一次页面节点更新完毕后触发
})

this.$nextTick(()=>{
// 下一次页面节点更新完毕后触发
})

created() {
// 本身created是拿不到dom节点
console.log(document.querySelector("#msg"));
// 但是nextTick是下一次dom更新后触发 ,就相当于mounted时触发回调函数
Vue.nextTick(() => {
console.log(document.querySelector("#msg"));
});

this.$nextTick(() => {
console.log(document.querySelector("#msg"));
});
}


2.4 Vue.filter

全局过滤器,注册的全局过滤器可以在任何页面使用

1
2
3
4
5
6
7
8
9
#main.js

Vue.filter('过滤器的名字',(参数)=>{
// 一堆格式处理的逻辑
return ’过滤后的结果‘
})



2.5 Vue.component

注册全局组件

1
2
3
4
5
// 全局组件
Vue.component('Counter', Counter)



2.6 Vue.use

使用插件,当插件是基于Vue.js写的,就需要use一下

1
2
3
4
5
6
7
8
import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';
import App from './App.vue';

// 因为ElementUI底层是基于Vue.js写的 所以需要Vue.use使用一下
Vue.use(ElementUI);


2.7 Vue.directive

自定义的指令,可以根据自身需求自己定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Vue.directive('指令名',{
// 里面有很多的配置
bind #只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。
inserted #被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)。

})

Vue.directive('overflow', {
inserted: (dom, obj) => {
// 截取 溢出隐藏
dom.style.width = obj.value + 'px'
dom.style.overflow = 'hidden'
dom.style.whiteSpace = 'nowrap';
dom.style.textOverflow = 'ellipsis';
}
})


作业

2 • 配置Fitness健身项目的路由(一级路由 和 二级路由)。

3 代码练习

4 xmind

5 下节课过一眼

0.0 课程介绍

  • Vuex 【掌握】
  • 页面懒加载
  • 组件懒加载

1.0 Vuex 状态管理库

Vuex是一个Vue的插件,用于管理状态( 数据 )

Vuex作用场景:

  • 祖先组件向子孙组件传递数据,层级非常深

  • 多个组件间的数据共享

Vue的核心功能

核心State

用于注册、存放数据的仓库

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
export default new Vuex.Store({
// 存储状态(数据)
state: {
num: 250,
goods: [
],
// 白马会员列表
whiteHorseVip: [
{ name: '凯子哥', age: 24 },
{ name: '黄子姐', age: 22 },
{ name: '维子哥', age: 26 },
{ name: '俊子哥', age: 18 }
]
}
})


1、直接获取仓库的数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<template>
<div>
<div>num:{{ $store.state.num }}</div>
<div>
goods:
<p v-for="item in $store.state.goods"
:key="item.name"
>{{ item.name }} --{{ item.price*item.count }}</p>
</div>
</div>
</template>

<script>
export default {
created() {
console.log(this.$store.state.num);
},
// 推荐
computed: {
num() {
return this.$store.state.num;
},
goods() {
return this.$store.state.goods;
}
},
};
</script>


2、通过辅助函数获取仓库数据

mapState 特点:map 映射 State仓库 : 与仓库里的state一一对应

1
2
3
4
5
6
7
8
9
10
11
12
13
import {mapState} from 'vuex'
export default{
computed:{
// 辅助函数的数组写法
...mapState(['要取变量1''要取变量2']), 【强烈推荐】
// 辅助函数对象写法
...mapState({
key:(state)=>state.要取变量1,
key1:(state)=>state.要取变量2,
})
}
}

核心Mutations

用于修改仓库数据的唯一方法,如果要修改仓库的数据必须提交一个mutation

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// 定义修改的方法,mutation
export default new Vuex.Store({
// 存储状态(数据)
state: {
num: 250,
goods: []
},
// 修改数据的唯一方法,如果要修改状态必须提交一个mutation
mutations: {
// 定义一个修改mutation - 函数名一般为全大写
函数名(state,payload){
state.goods = payload
}
},
})


// 在你的页面直接提交mutation变化,用于修改仓库的数据
this.$store.commit('函数名',实参)
this.$sotre.commit({ 【不推荐】
type:'函数名',
data:实参
})

// 通过辅助函数提交mutation
import {mapMutations} from 'vuex'

export default{
methods:{
// 辅助函数的数组写法
...mapMutations(['函数名'])
// 辅助函数的对象写法
...mapMutations({
key:'函数名1',
key2:'函数名2'
})
}

}

核心Getters

相当与状态管理库的计算属性,对state进行二次处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
// 定义getter处理方法
export default new Vuex.Store({
// 存储状态(数据)
state: {
num: 250,
goods: [
],
// 白马会员列表
whiteHorseVip: [
{ name: '凯子哥', age: 24 },
{ name: '黄子姐', age: 22 },
{ name: '维子哥', age: 26 },
{ name: '俊子哥', age: 18 }
],

},
// vuex里的计算属性
getters: {
// 把数据进行二次处理
whiteHorseVipList(state) {
return state.whiteHorseVip.filter(v => v.age > 18)
}
}
})

// 在页面获取getters里的属性
<template>
<div>
<div>num:{{ $store.getters.whiteHorseVipList }}</div>
<div>
goods:
<p v-for="item in $store.getters.whiteHorseVipList"
:key="item.name"
>{{ item.name }} --{{ item.age }}</p>
</div>
</div>
</template>

<script>
export default {
// 推荐
computed: {
whiteHorseVipList() {
return this.$store.getters.whiteHorseVipList;
}
},
};
</script>

// 辅助函数获取getters属性
import {mapGetters} from 'vuex'
export default{
computed:{
// 辅助函数的数组写法
...mapGetters(['whiteHorseVipList'])
}
}

核心Actions

用于异步修改数据,但是最终修改数据还是需要调用mutation方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
// 在仓库中定义actions
export default new Vuex.Store({
// 存储状态(数据)
state: {
num: 250,
},
// 修改数据的唯一方法,如果要修改状态必须提交一个mutation
mutations: {
SET_NUM(state, payload) {
state.num += payload
}
},
// 处理异步修改数据,最终也要提交一个mutation
actions: {
asyncSetNum(context, payload) { // context === new Vuex()
return new Promise(reslove => {
// 异步操作
setTimeout(() => {
context.commit('SET_NUM', payload)
reslove()
}, 2000);

})
}
},
})

// 在页面触发actions的方法

// 直接触发
this.$store.dispatch('函数名',实参)
this.$store.dispatch({ 【不推荐】
type:'函数名'',
data:'实参'
})

// 辅助函数
import {mapActions} from 'vuex'
export default{
methods:{
// 数组写法
...mapActions(['函数名']),

// 对象写法
...mapActions({
键名:'函数名'
})
}
}

核心modules

模块化,拆分你的仓库

1 定义模块化的仓库

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
export default new Vuex.Store({

// 模块,把仓库拆分成多个
modules: {
moduleA: {
namespaced: true,
state: {
money: 1000000
},
getters: {},
mutations: {},
actions: {}
},
moduleB: {
namespaced: true,
state: {
money: 10
},
getters: {},
mutations: {},
actions: {}
}
}
})


2 获取模块化里的状态(数据)

1
2
3
4
// 直接获取
this.$store.state.模块名.属性名;
// 辅助函数获取

修改模块里的数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
<template>
<div>
home
<h1>moneyA:{{ moneyA }}</h1>
<h1>moneyB:{{ moneyB }}</h1>

<div>money:{{ money }}</div>
<button @click="SET_MONEY(100)">改变</button>
<button @click="update">直接改变</button>
</div>
</template>

<script>
import { mapState, mapMutations } from "vuex";
export default {
created() {
console.log(this.$store);
},
computed: {
// 辅助函数mpaState获取模块里的数据
...mapState("moduleA", ["money"]),
moneyA() {
// 直接获取modelA里state的属性
return this.$store.state.moduleA.money;
},
moneyB() {
// 直接获取modelB里state的属性
return this.$store.state.moduleB.money;
}
},
methods: {
// 模块化里获取辅助函数
...mapMutations("moduleA", ["SET_MONEY"]),
update() {
// 直接调用模块化里的方法
this.$store.commit("moduleA/SET_MONEY", 200);
}
}
};
</script>



0.0 课程介绍

  • 动态组件 【掌握】
  • 修饰符 【掌握】
  • 混入mixins 【掌握】
  • 内置组件 【理解】
  • v-model 【理解】
  • 路由懒加载和组件懒加载(异步组件) 【掌握】
  • 递归组件 【了解】
  • Vue底层:MVVM架构模式 【理解】
  • 虚拟DOM 【了解】

#目标:希望把知识点总结话术,能够大概的说出相应的理论

1.0 动态组件

通过内置组件component 的属性is 进行组件的动态渲染

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<template>
<div>
<component :is="flag?Counter:HelloVue"></component>
<button @click="flag = !flag">取反{{ flag }}</button>
</div>
</template>

<script>
import Counter from './components/Counter.vue'
import HelloVue from '@/views/components/HelloVue.vue';
export default {
data(){
return {
flag:false,
Counter:Counter,
HelloVue
}
}
}
</script>


2.0 Vue修饰符

2.1 事件修饰符

.stop // 帮助你阻止事件冒泡

.prevent // 阻止默认行为

1
2
3
4
5
6
7
8
9
10
11
12
<template>
<div>
<div class="box1" @click="handleBox1">
// 阻止冒泡及默认行为
<a class="box2" @click.stop.prevent="handleBox2" href="https://www.baidu.com"></a>
</div>
// 阻止默认行为
<a href="https://www.baidu.com" @click.prevent>点我</a>
</div>
</template>


2.2 按键修饰符

•.enter // 输入回车时触发

•.space // 输入空格时触发

• .up 上

• .down 下

• .left 左

• .right 右

document.body.addEventListener(‘keyup’,(e)=>{console.log(e.keyCode)}) // 获取你输入的按键码

2.3 表单修饰符

.lazy // 将v-model输入事件从input改变成change事件

.number // 将输入的数据转换为number类型

.trim // 去除输入数据的首尾空格

2.4 .sync修饰符

1
2
3
4
5
6
7
8
9
10
11
12
13
# 父页面
<template>
# 注意: .sync === @update:value="msg = $event"
<div>
.sync修饰符{{ msg }}
<!-- <son :value="msg" @update:value="msg = $event"></son> -->
<son :value.sync="msg" ></son>
</div>
</template>

#子页面
this.$emit('update:value','嘿嘿')

3.0 mixins混入 【掌握】

可以抽取公共的配置选项,然后混入到你需要的页面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# mixin.js
export default {
data(){
return {
user:{name:'金花宝宝'}
}
},
methods:{
updateUser(){
this.user.name= '张无忌'
}
}
}

#在你需要混入的页面使用

<script>
import mixin from '@/mixins/mixin'
export default {
// 配置选项
mixins:[mixin],
data(){
return {
age:18
}
},
methods:{
getData(){
console.log('嘿嘿');
}
}
}
</script>


4.0 Vue内置组件

  • slot : 插槽
  • componet : 动态组件
  • transition : 过渡动画
1
2
3
4
5
6
7
8
9
10
11
12
13
<transition name="动画名">
// 里面放有dom操作的dom节点
<div v-show v-if></div>
</transition>

// 动画效果
.动画名-enter-active, .动画名-leave-active {
transition: opacity .5s;
}
.动画名-enter, .动画名-leave-to /* .fade-leave-active below version 2.1.8 */ {
opacity: 0;
}

  • keep-alive 缓存组件
1
2
3
4
5
6
7
8
9
<keep-alive include="['组件名a', '组件名b']" :max="20" :exclude="['不缓存的组件名','不缓存的组件名']">
// 包裹住你的出口 app.vue
<router-view></router-view>
</keep-alive>

:include="['组件名a', '组件名b']"
:exclude="['不缓存的组件名','不缓存的组件名']"
:max="20" 你最多缓存的组件数量

5.0 自定义的v-model

实现自定义v-model的方法

1、组件接收值的属性value

2、组件内修改这个值的自定义事件名为input

修改v-model的默认配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<template>
<div>
<!-- 将默认的属性从value改成heihei 将默认的事件从input改成change -->
<select :value="heihei" @change="inputData">
<option value="A">A</option>
<option value="B">B</option>
<option value="C">C</option>
<option value="D">D</option>
</select>
</div>
</template>
export default {
model: {
prop: 'heihei',
event: 'change'
},
props:{
// 接收的属性value
heihei:{
type:String,
default:0
}
},
methods:{
inputData(e){
console.log(e.target.value);
// 输出的事件名 input
this.$emit('change',e.target.value)
}
}
}

6.0 路由懒加载 和 组件懒加载(异步组件)

路由懒加载的作用:

只会根据输入路径之后,再进行针对性组件页面加载,提高首屏加载速度

1
2
3
4
5
6
{
path: '/sync-modifier',
name: 'sync-modifier',
component: () => import('../views/02-.sync修饰符.vue')
}

组件懒加载(异步组件)

在页面渲染到指定位置才开始加载对应的组件

1
2
3
4
5
components:{
MyInputNumber:()=>import('./components/MyInputNumber.vue')
},


7.0 递归组件 【理解】

组件自己调用自己

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<template>
<div>
<ul >
<li v-for="item in dataList" :key="item.name">{{ item.name }}
<my-menu :dataList="item.children"></my-menu>
</li>
</ul>
</div>
</template>

<script>
export default {
name:'MyMenu',
props:{
dataList:{
type:Array,
default:()=>[]
}
}
}
</script>


8.0 MVVM的底层原理

M:Model 数据模型,主要是提供数据

V: View 视图,主要用于渲染页面

VM: ViewModel视图模型 , 他是连接 M 和V的桥梁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// Model
const data = {
message:'这是初始的值'
}

// View
function render(){
document.querySelector('#app').innerHTML = `
<input value=${data.message} oninput="inputchange(this)"/>
${data.message}
`
}
render()

let val = data.message
// ViewModel 是M 和 V之间的桥梁
// 有三个参数 参数1:需要挟持的对象 参数2:需要监听键名 参数3:是个对象,里面有两方法 get set
// 通过defineProperty 将所有操作(访问,修改),转换成get和set
Object.defineProperty(data,'message',{
// 只要有人访问了挟持对象的那个属性就触发get
get(){
console.log('get');
return val
},
// 只要有人修改挟持对象的那个属性就触发set
set(newVal){
console.log('set');
val = newVal
render()
}
})

// V 到 M 的方向
function inputchange(dom){
console.log(dom.value);
data.message = dom.value

}


9.0 虚拟DOM

虚拟是一个js的对象,vue中虚拟dom在beforeMount创建完毕,dom真实节点尚未挂载,当页面发生改变,再次创建一个新的虚拟dom,通过diff算法,将新老虚拟dom,进行比较差异,得到差异地方,进行针对性的局部渲染,大大提高了页面性能,减少不必要的消耗。

diff算法:

  • 深度优先原则
  • 同层比较原则

• v-for的key并不会在页面渲染,key就是提供给虚拟dom对比使用,优化性能.

作业

整理一份xmind, 包含6天所有vue的知识(并且回顾)

Welcome to Hexo! This is your very first post. Check documentation for more info. If you get any problems when using Hexo, you can find the answer in troubleshooting or you can ask me on GitHub.

Quick Start

在blog文件夹打开git bash

创建文章

1
$ hexo new "文章名称"

More info: Writing

1.清除缓存

1
$ hexo clean

2.本地发不测试

1
$ hexo server || hexo s

More info: Server

3.生成静态文件

1
$ hexo generate || hexo g

More info: Generating

4.部署到远程

1
$ hexo deploy || hexo d

More info: Deployment

proxy配置
浏览器有同源策略,直接从浏览器访问服务器,为了页面安全 则会判断 端口/域名/协议是否相同。我们可以通过在本地创建服务器来进行访问后台服务器,
本地开发时使用,配置中间代理用于转发ajax 解决跨域问题
在vue.config.js的 defineConfig 中配置

1
2
3
4
5
6
7
8
9
10
11
12
13
devServer:{
port:' 9202 '
proxy:{
'/api':{
//真正地址通过target
target:'http://基本地址'

//路径重写,将api替换
pathRewrite:{'^/api': ' '}

}
}
}

脚手架的模式与环境变量:
开发模式:development
测试模式:test
生产模式:production

在根目录下直接创建文件来指定环境变量:
.env.development 在开发模式下被载入
.env.production 在生产模式下被载入

一个环境文件只包含环境变量的“键=值”对:

一般以VUE_APP开头

基本的服务器路径:

VUE_APP_NOT_SECRET_CODE=’/api’ —开发文件中
VUE_APP_NOT_SECRET_CODE=’服务器路径’ —–生产文件中

还需要再公共资源地址配置的vue.config.js中配置 publicPath:’’

文件流处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
axios的使用
axios.get('地址',{
//识别为文件流blob|arrarBuffer
responseType:'blob'
}).then(res=>{
//通过a 标签实现下载
const a = document.createElement('a')

//创建一个下载的地址 URL.creatObject()
const URL = URL.createObject(res.data)

//将a.herf的地址设置为URL
a.herf = URL

//a 标签的下载
a.download= '文件名' //最好是取请求头里的内容

//调用a的点击
a.click()
})

假数据Mock.js的使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
- yarn add mockjs
- yarn add @types/mockjs -D (ts的时候要)

执行mock/index.ts. 就是在main.ts中引入index.ts文件即可

引入:import Mock from 'mockjs'
注册:Mock.mock('地址',’方式‘,返回的数据)

工程化mock:

分模块:1.模块名.mock.js
2.在文件夹里面导出一个一个mock接口对象
export const goodsInfo = {
url: '/goods/info',
method: 'get',
data: () => {
// 伪造数据
return 'userInfo'
}
}
创建mock/index.js
引入所有的接口对象

// 全自动导入所有mock
// 获取到所有的
let module1 = import.meta.glob('./*.mock.ts')
console.log(module1);

Object.values(module1).forEach((item) => { //得到每一个方法
// console.log(item());

item().then((v: any) => { //调用后会得到一个promise

Object.values(v).forEach((d: any) => { //得到每一个mock接口函数得数据
console.log(d);
Mock.mock(d.url, d.methods, d.data)

// console.log(d);

})
})
})

// 半自动
// let arr = [goods, homes]
// import * as goods from './goods.mock'
// import * as homes from './home.mock'
// arr.forEach((item) => {
// Object.values(item).forEach(v => {
// Mock.mock(v.url, v.methods, v.data)
// })
// })
// for (let k in item) {
// Mock.mock(item[k].url, item[k].methods, item[k].data)
// }

XLSX.js处理excecl文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
yarn add xlsx 

引入:
import * as XLSX from 'xlsx'

核心api

- XLSX.utils.book_new() 新建工作簿
- xlsx.utils.json_to_sheet(json数组)创建一个sheet,然后给该sheet添加数据
- xlsx.utils.aoa_to_sheet(二维数组)创建工作表 数组格式
- xlsx.utils.book_append_sheet: 往工作簿里面添加一个sheet
- xlsx.writeFile(工作簿,名称,配置项) 下载生成的excel

实例:
// 1. 创建一个工作簿 workbook
const workBook = xlsx.utils.book_new()
// 2. 创建工作表 worksheet
const workSheet = xlsx.utils.json_to_sheet([
{
id: 1, name: '张三', age: 16
},
{
id: 2, name: '李四', age: 18
},
{
id: 3, name: '王五', age: 20
}
])
// 3. 将工作表放入工作簿中
xlsx.utils.book_append_sheet(workBook, workSheet)
// 4. 生成数据保存
xlsx.writeFile(workBook, `测试.xlsx`, {
bookType: 'xlsx'
})



Excel导入
通过Elemenui 有上传可得到二进制流的数据。

const readExecl = (e) => {
//1 获取文件信息
let file = e.raw // 文件信息
//创建读取器
const fileReader = new FileReader()

//2 开始读取文件的内容为二进制
fileReader.readAsBinaryString(file)

//3 读取完成
fileReader.onload = (ev) => {

const data = ev.target.result

//4 读取工作簿
const workbook = XLSX.read(data, {
type: 'binary', // 以字符编码的方式解析
})

//5 获取工作表
const exlname = workbook.SheetNames[0]
// 取第一张表

//6 把工作表变成json格式
const exl = XLSX.utils.sheet_to_json(workbook.Sheets[exlname])


//生成json表格内容
console.log(exl)
}

}


导入要将文件转为二进制数据 原生JS有个input框的type属性值为file

UI库可直接拿到数据,例如:e.row是转换的数据
1.浏览器可以读取文件了 通过let fileReader = new FilReader()
2.通过fileReader 的 readAsBinryString(e.row)
3.通过读取成功时的函数loud
4.fileReader.loude=(ev)=>{}函数得到一个读取成功后的数据
5.XLSX读取数据 let book =XLSX.read(ev.target.rults,{type:'binary'})
6.拿到一页数据let sheet= book.SheetName[0]
7.根据sheet的名字拿到对应的内容,通过XLSX.utiles.sheet_to_json拿到转化的数据

无感刷新token

axios&token

  • 请求拦截器

    • 从本地把token拿出来,然后放入header中,带过后端
  • 响应式拦截器

    • 拦截401错误码,跳转到登录页面。【token的过期于否是后端规定】
  • 问题:

    • 场景:假设当前这个token还有10秒过期,用户这个时候填写了一个特别长的表单(填写时间超过10s)。用户辛辛苦苦填写表单,一下子没了(axios拦截到401之后跳转到登录页面)。用户体验非常差。

刷新token(双token刷新策略)

  • 登录成功之后,后端返回两个token
    • access_token: 请求的时候使用 ——-》 过期时间比较短
    • refresh_token: 用来刷新token(换取一个新的token) —-》过期事件比较长
  • 以后发起请求都带上access_token
    • 正常的请求
  • access_token失效之后
    • 以前:拦截401,跳转到登录页面。【糟糕的用户体验】
    • 现在:重新发起一个请求(刷新token),得到一个新的token。(缓存之前的接口)重新发起请求。

pinia vuex升级版 vue3 专用状态管理

Pinia 最初是在 2019 年 11 月左右重新设计使用 Composition API 。从那时起,最初的原则仍然相同,但 Pinia 对 Vue 2 和 Vue 3 都有效,并且不需要您使用组合 API。 除了安装和 SSR 之外,两者的 API 都是相同的,并且这些文档针对 Vue 3,并在必要时提供有关 Vue 2 的注释,以便 Vue 2 和 Vue 3 用户可以阅读

vuex有啥问题

  • vuex集中式管理状态

  • 流程复杂

    • state(放数据) —》 mutation(同步修改数据) —》 action(操作异步,调用mutation)
    • 建议: mutation 和 action可以合并在一起
  • 模块机制

    • 数据结构(属性)

    • 每个模块下的mutation和action都是挂载到跟模块,为了区分各个模块mutaion,分配了一个命名空间

    • 上诉步骤确实区分开各个模块的mutaion/action。。。 代码访问的时候就复杂了。

      • 例子: 我要调用模块seller下的一个M_name

        1
        this.$store.commit('seller/M_name')
      • 例子: 我要调用模块seller下的模块test下的M_name

        1
        this.$store.commit('seller/test/M_name')
      • 例子,我要访问seller下的name

        1
        this.$store.state.seller.name

为什么要使用 Pinia?#

Pinia 是 Vue 的存储库,它允许您跨组件/页面共享状态。 如果您熟悉 Composition API,您可能会认为您已经可以通过一个简单的 export const state = reactive({}). 这对于单页应用程序来说是正确的,但如果它是服务器端呈现的,会使您的应用程序暴露于安全漏洞。 但即使在小型单页应用程序中,您也可以从使用 Pinia 中获得很多好处:

  • dev-tools 支持
    • 跟踪动作、突变的时间线
    • Store 出现在使用它们的组件中
    • time travel 和 更容易的调试
  • 热模块更换
    • 在不重新加载页面的情况下修改您的 Store
    • 在开发时保持任何现有状态
  • 插件:使用插件扩展 Pinia 功能
  • 为 JS 用户提供适当的 TypeScript 支持或 autocompletion
  • 服务器端渲染支持
  • 没有mutations,只有action(写同步,也可以写异步)
  • 不需要modules, 多个仓库,你可以理解为一个仓库就是一个模块
  • 不需要命名空间
  • dev-tools变得友好
  • 编码风格
    • vuex的编码风格: 配置型的
    • pinia支持配置型api也支持组合式api【推荐】

安装pinia

1
2
3
yarn add pinia
# 或者使用 npm
npm install pinia

使用

  • 全局main.js 配置pinia

    1
    2
    3
    4
    5
    6
    7
    const app = createApp(App)

    #引入&使用pinia
    import {createPinia} from 'pinia'
    app.use(createPinia())

    app.mount('#app')
  • 语法1 store/numStore.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    /* num pinia模块 */
    import {defineStore} from 'pinia'

    export const useNumstore = defineStore('numstore', {
    //state数据
    state: () => {
    return {
    num: 10,
    }
    },
    //getters
    getters: {
    doubleNum(state) {
    return state.num * 2
    },
    },
    //actions
    actions: {
    //增加
    increase() {
    this.num++
    },
    //减少
    decrease() {
    this.num--
    },
    },
    })

  • 语法2 hooks语法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    /* num pinia模块 */
    import {defineStore} from 'pinia'
    import {ref,computed} from 'vue'
    export const useNumstore = defineStore('numstore', ()=>{
    const num = ref(10)
    const doubleNum = computed(()=>num.value * 2)
    const increase = ()=>{
    num.value++
    }
    const decrease = ()=>{
    num.value--
    }
    return {
    num,
    doubleNum,
    increase,
    increase
    }
    })
  • 使用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    <template>
    <div>
    <h3>子组件</h3>
    <p>数据num:{{ num }}</p>
    <button @click="increase">+</button>
    <button @click="decrease">-</button>
    </div>
    </template>

    <script setup>
    #导出解构响应式函数
    import {storeToRefs, computed} from 'pinia'
    #引入仓库
    import {useNumstore} from '../../store/modules/numStore'
    #创建一个hooks 仓库实例
    const $numStore = useNumstore()
    #函数直接解构
    const {increase, decrease} = $numStore
    #数据需要响应式解构
    const {num} = storeToRefs($numStore)

    #获取响应式数据方法2 简单
    const num = computed(()=>$numStore.num)
    </script>