vue3

404 阅读18分钟

VUE3

开发工具

VSCode Volar devtools

构建开发环境

Vite 对node版本要求14.20+

npm create vite@latest  或 npm init vue@latest
cd 你的项目目录
npm install

开发

npm run dev

打包

npm run build

配置开发环境vite.config.js

export default defineConfig({
  server: {
    port: 8080,
  }
})

单文件组件(SFC)

可以理解为一个可以书写html、css、js的页面容器,vue的一张页面就可以是一个*.vue的组件

app.vue

<template>
	在这里写html
</template>
<script>
	在这里写js
</script>
<style>
	在这里写样式
</style>

但其实一张页面可以有多个这样的组件组成,以后再说

js书写风格

选项式

暴露一个对象,对象包含多个选项的键值对来约定逻辑,面向类与实例的一种编写习惯

<script>
//实例化  ~~ new Xxx({选项:value})
export default {

  选项:{
  	key:..
	}
  选项: {
    xx:function() {
      this.count++   // this 指向实例
    }
  },

}
</script>

组合式

script标签里面与平时书写的js一样,再配合es模块化导入一些函数,或者自定义一些函数来解决逻辑,没有类,也没有this的概念

<script setup>
import { xx, oo } from 'vue'

const count = xx(0)

function show() {
  //..
}
  
oo()
</script>

该选哪一个

选项式 API 对初学者而言更为友好,沿用了vue2习惯,如果你进小公司做中小型项目,可使用

组合式 API 构建更大更完整的项目时推荐,进BAT首选,属于vue3新推

响应式

数据的更新页面会随之变化,无需手动操作dom

组合式

reactive

  • 一次定义多条数据的响应式
  • 一般为对象或者数组
  • 响应式转换是“深层的”:会影响对象内部所有嵌套的属性
  • 内部基于 ES6 的 Proxy来实现对对象内部所有数据的劫持, 并通过Reflect操作对象内部数据
<template>
	{{data.数据名}}
</template>

<script setup>
import { reactive} from 'vue'
const data = reactive({ 数据名: 值,x:o,... })
data.数据名 //访问
data.数据名=值 //修改
</script>

ref

  • 一次定义一条数据(一般为基本类型)的响应式
  • 创建一个包含响应式数据的引用(reference)对象
  • 如果ref一个对象/数组, 内部会自动将对象/数组转换为reactive的结果
  • 通过给value属性添加getter/setter来实现对数据的劫持
<template>
	{{数据名}}
</template>

<script setup>
import { ref} from 'vue'
const 数据名 = ref(值)
数据名.value //访问
数据名.value=值 //修改
</script>

事件绑定

<template>
	<button v-on:不带on的源生事件名="方法"..
  <button	@事件名="方法"	...
  <button	@事件名="方法(参数)" .....
  <button	@事件名="方法($event,参数)"	.....
</template>

事件名 不带on

<!--组合式-->
<script setup>
function 函数(参数,事件对象){
  业务
}

const 函数 = (参数,事件对象 ) => {
  业务
}
</script>

事件对象

事件对象可以不传递,需要传递的场景,传参数同时使用事件对象时,如:show($event,参数)

key的问题

状况:面向数据操作,如数组的索引位置的内容删除,索引还在的情况

解决:给指定循环的dom一个key 是数据的id,确保key唯一性,避免数据错乱导致的视图问题,同时提供性能优化

原因:key特殊属性主要用作 Vue 的虚拟 DOM 算法在将新节点列表与旧列表进行比较时识别 vnode 的提示, 在没有键的情况下,Vue 使用一种算法来最小化元素移动,并尝试尽可能多地就地修补/重用相同类型的元素

计算属性

模板中的表达式虽然方便,但也只能用来做简单的操作。如果在模板中写太多逻辑,会让使其变得臃肿,难以维护

计算属性是一个函数,所依赖的元数据变化时,会再次执行,平时会缓存,是响应式的,需要在模板中渲染才可调用

组合式

<script setup>
import { reactive, computed } from 'vue';
  
const data = reactive({
  str: 'i love you',
})

const cptStr = computed(() => data.str.split(' ').reverse().join(' '))
</script>

<tempalte>
	{{计算属性}} 
	<div v-指令="计算属性"></div>
	<img :[计算属性]="计算属性">
</tempalte>

computed VS method

函数computed
方法会每次调用基于它们的响应式依赖进行缓存的
一般性能高
{{methodname()}}{{computedname}}
适合强制执行和渲染适合做筛选

属性检测

需要在数据变化时执行一些副作用业务(异步,dom等开销较大的操作)

组合式

watch函数

  • 可监听ref/reactive/getter函数/数组
  • 监视指定的一个或多个响应式数据, 一旦数据变化, 就自动执行回调
  • 默认初始时不执行, 通过配置immediate为true, 来指定初始时执行
  • 通过配置deep为true, 来指定深度监视

watchEffect函数

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

import { reactive, ref, watch, watchEffect } from 'vue';

const data = reactive({
  count: 1,
  obj: { count: 1, b: 2 },
})

const counter = ref(0)

const checkData = () => {
  // data.count++
  // data.obj.count++
  // data.obj.b++
  // data.obj = { a: 11, b: 22 }
  counter.value++
}

//监听 ref
watch(counter, (newValue, oldValue) => {
  console.log('watch-ref', newValue, oldValue)
})

//监听reactive 不推荐 有性能消耗
watch(data, (newValue, oldValue) => {
  console.log('监听reactive', newValue, oldValue)
})

//监听reactive的属性,需要提供一个getter函数
watch(() => data.count, (newValue, oldValue) => {
  console.log('watch-count', newValue, oldValue)
})

//监听reactive的obj
watch(() => data.obj, (newValue, oldValue) => {
  console.log('watch-obj', newValue, oldValue)
})

//深度监听 有性能损耗
watch(() => data.obj, (newValue, oldValue) => {
  console.log('watch-obj.key-deep', newValue, oldValue);//因为它们是同一个对象!
}, { deep: true })

//深度监听 + 首次运行
watch(() => data.obj, (newValue, oldValue) => {
  console.log('watch-obj.key-deep', newValue, oldValue);//因为它们是同一个对象!
}, { deep: true, immediate: true })

//首次运行 
watchEffect(() => {
  data.count;//函数里面出现过的响应式属性,只要变化了就再次会执行
  console.log('watchEffect-count')
})

//多个属性一并检测 相比选项式,可以把多个数据变化的业务写到一起
watch([counter, () => data.obj.b], ([newCounter, newB], [oldCounter, oldB]) => {
  console.log('watch-ref+data.count', newCounter, oldCounter);
})

计算属性 VS 函数 VS 属性检测

计算属性函数属性检测
依赖模板调用且有返回值-×
是否缓存×
异步×

style标签中启用动态 CSS

css里直接可以使用变量

<script setup>
const color = ref('yellow')
const data = reactive({color2:'red'})
</script>
<style>
div {
  color: v-bind("color");
  color2: v-bind('data.color');/*表达式需要引号*/
}
</style>

数据请求

向服务器发送ajax|表单请求,抓取数据,【vue:你看我看嘛】

解决方案

  • 自行通过XMLHttpRequest对象封装一个ajax
  • 使用第三方自带的ajax库,如:jquery ×
  • 使用js原生自带的promise语法糖api fetch
  • 使用第三方ajax封装成promise习惯的库,如:axios

跨域

有时,前端和后端的工程文件不在同一个域,也会出现跨域,以下是解决方案

后端解决方案

部分接口允许

//node 要允许的接口内部 
res.setHeader('Access-Control-Allow-Origin', req.headers.origin)

//php端
header('Access-Control-Allow-Origin:*');

所有接口允许

//node端
let cors = require('cors');

app.use(cors({
  //允许所有前端域名
  "origin": "*",  
  "credentials":true,//允许携带凭证
  "methods": "GET,HEAD,PUT,PATCH,POST,DELETE", //被允许的提交方式
  "allowedHeaders":['Content-Type','Authorization','token']//被允许的post方式的请求头
}));


前端解决方案

  • jsonp接口

  • 浏览器装插件 正向代理

  • 开发环境做代理(webpack/vite,反向代理,客户端代理)

客户端代理 vite.config.js

server: {
    port: 8080,
    proxy: {
      "/api": {//组件请求是使用 /api/..
        //代理到
        target: "http://localhost:9001",

        changeOrigin: true, //开启代理

        //别名替换
        // rewrite: (path) => path.replace(/^\/api/, ""),

        // ws: true, //socket协议开启
      },
      "/book": {
        //目标代理到线上服务器
        target: "https://api.zhuishushenqi.com",

        changeOrigin: true, //开启代理
        //别名替换
        // rewrite: (path) => path.replace(/^\/api/, ""),
        // ws: true, //socket协议开启
      },
    },
  },

读取第三方接口

axios({
  url: '/book/57206c3539a913ad65d35c7b',
  params: {
    start: 0,
    count: 3
  }
}).then(
  res => this.list = res.data
)

mock

JSON-Server 是一个 Node 模块,运行 Express 服务器,你可以指定一个 json 文件作为 api 的数据源。

安装json-server

npm init -y   # 生成开发环境
npm i json-server -S  #安装依赖包

启动 json-server

json-server可以直接把一个json文件托管成一个具备全RESTful风格的API,并支持跨域、jsonp、路由订制、数据快照保存等功能的 web 服务器。

db.json文件的内容:

{
  "course": [
    {
      "id": 1000,
      "course_name": "马连白米且",
      "autor": "袁明",
      "college": "金并即总变史",
      "category_Id": 2
    },
    {
      "id": 1001,
      "course_name": "公拉农题队始果动",
      "autor": "高丽",
      "college": "先了队叫及便",
      "category_Id": 2
    }
  ]
}

例如以下命令,把db.json文件托管成一个 web 服务。

$ json-server --watch --port 53000 db.json

输出类似以下内容,说明启动成功。

\{^_^}/ hi!

Loading db.json
Done

Resources
http://localhost:53000/course

Home
http://localhost:53000

Type s + enter at any time to create a snapshot of the database
Watching...

此时,你可以打开你的浏览器,然后输入:http://localhost:53000/course

RESTful API

POST /user  !address中包含数据
删 DELETE /user/:id | user?id=1 根据ID删除用户信息
改 PUT|PATCH /user !address中包含数据 PUT覆盖修改 PATCH局部修改
查 GET /user /user/1 | user?id=1 
	 GET  根据用户id查询用户数据 没有id查询所有 /1 返对象 id=1 返回数组>对象
分页	_page 第几页, _limit一页多少条
  GET /user?_page=7  不传递默认0
  GET /user?_page=7&_limit=20 不传递默认所有
排序 _sort设定排序的字段 _order设定排序的方式(默认升序)
  GET /user?_sort=views&_order=asc
  GET /user/1/comments?_sort=votes&_order=asc
  GET /user?_sort=title,views&_order=desc,asc 	多个字段排序
任意切片数据 _start 开始不包含  _end 结束包含
  GET /users?_start=20&_end=30
  GET /user/1/comments?_start=20&_end=30
  GET /user/1/comments?_start=20&_limit=10
全文检索	GET /user?q=九哥

json-server 的相关启动参数

  • 语法:json-server [options] <source>
  • 选项列表:
参数简写默认值说明
--config-c指定配置文件[默认值: "json-server.json"]
--port-p设置端口 [默认值: 3000]Number
--host-H设置域 [默认值: "0.0.0.0"]String
--watch-wWatch file(s)是否监听
--routes-r指定自定义路由
--middlewares-m指定中间件 files[数组]
--static-sSet static files directory静态目录,类比:express的静态目录
--readonly--roAllow only GET requests [布尔]
--nocors--ncDisable Cross-Origin Resource Sharing [布尔]
--nogzip, --ng Disable GZIP Content-Encoding [布尔]
--snapshots-SSet snapshots directory [默认值: "."]
--delay-dAdd delay to responses (ms)
--id-iSet database id property (e.g. _id) [默认值: "id"]
--foreignKeySuffix--fks Set foreign key suffix (e.g. _id as in post_id)[默认值: "Id"]
--help-h显示帮助信息[布尔]
--version-v显示版本号[布尔]
  • source可以是json文件或者js文件。实例:
json-server --watch -c ./jsonserver.json
json-server --watch db.js  命令行里面要的db是个函数
json-server db.json
json-server --watch -port 8888 db.json

动态生成模拟数据

启动json-server的命令:json-server --watch db.js 是把一个js文件返回的数据托管成web服务。

app.js配合mockjs库可以很方便的进行生成模拟数据。

// 用mockjs模拟生成数据
var Mock = require('mockjs');

module.exports = () => {
  // 使用 Mock
  var data = Mock.mock({
    'course|227': [
      {
        // 属性 id 是一个自增数,起始值为 1,每次增 1
        'id|+1': 1000,
        course_name: '@ctitle(5,10)',
        autor: '@cname',
        college: '@ctitle(6)',
        'category_Id|1-6': 1
      }
    ],
    'course_category|6': [
      {
        "id|+1": 1,
        "pid": -1,
        cName: '@ctitle(4)'
      }
    ]
  });
  // 返回的data会作为json-server的数据
  return data;
};

路由

默认的路由

json-server为提供了GET,POST, PUT, PATCH ,DELETE等请求的API,分别对应数据中的所有类型的实体。

# 获取所有的课程信息
GET    /course

# 获取id=1001的课程信息
GET    /course/1001

# 添加课程信息,请求body中必须包含course的属性数据,json-server自动保存。
POST   /course

# 修改课程,请求body中必须包含course的属性数据
PUT    /course/1
PATCH  /course/1

# 删除课程信息
DELETE /course/1

# 获取具体课程信息id=1001
GET    /course/1001

自定义路由

当然你可以自定义路由:

$ json-server --watch --routes route.json db.json

route.json文件

{
  "/api/*": "/$1",    //   /api/course   <==>  /course
  "/:resource/:id/show": "/:resource/:id",
  "/posts/:category": "/posts?category=:category",
  "/articles\\?id=:id": "/posts/:id"
}

自定义配置文件

通过命令行配置路由、数据文件、监控等会让命令变的很长,而且容易敲错,可以把命令写到npm的scripts中,但是依然配置不方便。

json-server允许我们把所有的配置放到一个配置文件中,这个配置文件默认json-server.json;

例如:

{
  "port": 53000,
  "watch": true,
  "static": "./public",
  "read-only": false,
  "no-cors": false,
  "no-gzip": false,
  "routes": "route.json"
}

使用配置文件启动json-server:

# 默认使用:json-server.json配置文件
$ json-server db.js  
$ json-server db.json 

# 指定配置文件
$ json-server --watch -c jserver.json db.json

过滤查询

查询数据,可以额外提供

GET /posts?title=json-server&author=typicode
GET /posts?id=1&id=2

# 可以用 . 访问更深层的属性。
GET /comments?author.name=typicode

还可以使用一些判断条件作为过滤查询的辅助。

GET /posts?views_gte=10&views_lte=20

可以用的拼接条件为:

  • _gte : 大于等于
  • _lte : 小于等于
  • _ne : 不等于
  • _like : 包含
GET /posts?id_ne=1
GET /posts?id_lte=100
GET /posts?title_like=server

分页查询

默认后台处理分页参数为: _page 第几页, _limit一页多少条。

GET /posts?_page=7
GET /posts?_page=7&_limit=20

默认一页10条。

后台会返回总条数,总条数的数据在响应头:X-Total-Count中。

排序

  • 参数: _sort设定排序的字段
  • 参数: _order设定排序的方式(默认升序)
GET /posts?_sort=views&_order=asc
GET /posts/1/comments?_sort=votes&_order=asc

支持多个字段排序:

GET /posts?_sort=user,views&_order=desc,asc

任意切片数据

GET /posts?_start=20&_end=30
GET /posts/1/comments?_start=20&_end=30
GET /posts/1/comments?_start=20&_limit=10

全文检索

可以通过q参数进行全文检索,例如:GET /posts?q=internet

其他高级用法

json-server本身就是依赖express开发而来,可以进行深度定制。细节就不展开,具体详情请参考官网

const jsonServer = require('json-server');//在node里面使用json-server包
const db = require('./db.js');//引入mockjs配置模块
const path = require('path');
const Mock = require('mockjs');
let mock='/mock';//定义路由根别名

//创建服务器
const server = jsonServer.create();//创建jsonserver 服务对象


//配置jsonserver服务器 中间件
server.use(jsonServer.defaults({
  static:path.join(__dirname, '/public'),//静态资源托管
}));
server.use(jsonServer.bodyParser);//抓取body数据使用json-server中间件


//响应
server.use((request, res, next) => {//可选 统一修改请求方式
  // console.log(1)
  // request.method = 'GET';
  next();
});

//登录注册校验
let mr = Mock.Random;//提取mock的随机对象
server.get(mock+'/login', (req, res) => {
  // console.log(req.query, req.body);//抓取提交过来的query和body
  let username=req.query.username;
  let password=req.query.password;
  (username === 'aa' && password === 'aa123')?
    res.jsonp({
      "err": 0,
      "msg": "登录成功",
      "data": {
        "follow": mr.integer(1,5),
        "fans": mr.integer(1,5),
        "nikename": mr.cname(),
        "icon": mr.image('20x20',mr.color(),mr.cword(1)),
        "time": mr.integer(13,13)
      }
    }) :
    res.jsonp({
      "err": 1,
      "msg": "登录失败",
    })

});
server.post(mock+'/reg', (req, res) => {
  let username=req.body.username;
  (username !== 'aa') ?
    res.jsonp({
      "err": 0,
      "msg": "注册成功",
      "data": {
        "follow": mr.integer(0,0),
        "fans": mr.integer(0,0),
        "nikename": mr.cname(),
        "icon": mr.image('20x20',mr.color(),mr.cword(1)),
        "time": mr.integer(13,13)
      }
    }) :
    res.jsonp({
      "err": 1,
      "msg": "注册失败",
    })

});

//响应mock接口 自定义返回结构 定义mock接口别名
const router = jsonServer.router(db);//创建路由对象 db为mock接口路由配置  db==object

router.render = (req, res) => {//自定义返回结构
  let len = Object.keys(res.locals.data).length; //判断数据是不是空数组和空对象
  // console.log(len);

  setTimeout(()=>{//模拟服务器延时
    res.jsonp({
      err: len !== 0 ? 0 : 1,
      msg: len !== 0 ? '成功' : '失败',
      data: res.locals.data
    })
  },1000)

  // res.jsonp(res.locals.data)

};

server.use(jsonServer.rewriter({//路由自定义别名
  [mock+"/*"]: "/$1",

  // "/product\\?dataName=:dataName": "/:dataName",
  // "/banner\\?dataName=:dataName": "/:dataName",
  // "/detail\\?dataName=:dataName&id=:id": "/:dataName/:id",

  // "/product/del\\?dataName=:dataName&id=:id": "/:dataName/:id",
  // "/product/add\\?dataName=:dataName": "/:dataName",
  // "/product/check\\?dataName=:dataName&id=:id": "/:dataName/:id"
}));

server.use(router);//路由响应



//开启jsonserver服务
server.listen(3333, () => {
  console.log('mock server is running')
});

组件拼装

一个web应用应该有若干个组件拼装而成,考虑到复用的部分,可以把其 UI(HTML,CSS,JS) 划分为独立的SFC

component转存失败,建议直接上传图片文件

定义组件

<!-- 组件文件名.vue -->
<template>非必须-可以没有结构</template>
<script>非必须-可以没有逻辑</script>
<style>非必须-可以没有样式</style>

注册组件

//全局注册
app.component('组件标签名',组件变量名);


//局部注册-组合型
import 组件变量名 from '组件文件名'

调用组件

<组件标签名></组件标签名>

书写风格

  • 组件变量名: XxxXxx
  • 组件标签名:
    • 调用时(xxx-xxx | XxxXxx )
    • 注册时:XxxXxx
  • 组件文件名:xxx-xxx| XxxXxx

生命周期

每个 Vue 实例在被创建时都要经过一系列的初始化过程——例如,需要设置数据监听、编译模板、将实例挂载到 DOM 并在数据变化时更新 DOM 等。同时在这个过程中也会运行一些叫做生命周期钩子的函数,这给了用户在不同阶段添加自己的代码的机会

转存失败,建议直接上传图片文件

组合式

<script setup>
import { onBeforeMount, onBeforeUnmount, onBeforeUpdate, onMounted, onUnmounted, onUpdated, ref } from 'vue';
console.log('beforeCreated-无法访问数据 无法操作dom', msg1.value)
const msg1 = ref('Child数据1')
console.log('--------created-无法操作dom', msg1.value)

onBeforeMount(() => console.log('onBeforeMount'))
onMounted(() => 操作数据  操作dom)
onBeforeUpdate(() => 不要操作数据 会造成死循环 钩子是否执行,取决于模板是否使用了依赖的数据)
onUpdated(() =>不要操作数据 会造成死循环 钩子是否执行,取决于模板是否使用了依赖的数据)
onBeforeUnmount(() => 做一些清除副作用的的行为  取消定时器 终端请求大数据 停止运动)
onUnmounted(() => 做一些清除副作用的的行为  取消定时器 终端请求大数据 停止运动)
  
</script>

组件事件

组件绑定原生事件,默认绑定到组件根元素,可通过emit或者冒泡调用到,而组件内部显示声明emits接受后只能通过emit调用, 确保您的所有组件都使用该emits选项记录其事件

//给组件绑定事件
<自定义组件  v-on:自定义事件="函数" />
<自定义组件  @自定义事件="函数" />
<自定义组件  @原生js事件="函数" />

const emits = defineEmits(["自定义事件","原生js事件"]);
emits("自定义事件|原生js事件", 参数);

自定义事件名: 使用 kebab-case 的事件名

组件通讯基础

父子(props)

父组件通过属性绑定,子组件通过选项props接收,props是响应式的,props完成单向下行绑定

父调子时传

<子 :自定义属性="父数据"></..>

子收

<div>
  {{自定义属性}}
</div>


<!--组合式-->
<script setup>
//defineProps 宏方法 无需引入 
const props = defineProps(['自定义属性'])
console.log(props.自定义属性)
</script>

props 校验

在开发为其他人提供的组件时,或使用第三方提供的组件时会非常有用

//选项式
<script>
export default { 
  props: {
    // 基础类型检查
    //(给出 `null` 和 `undefined` 值则会跳过任何类型检查)
    propA: Number,
    // 多种可能的类型
    propB: [String, Number],
    // 必传,且为 String 类型
    propC: {
      type: String,
      required: true
    },
    // Number 类型的默认值
    propD: {
      type: Number,
      default: 100
    },
    // 对象类型的默认值
    propE: {
      type: Object,
      // 对象或者数组应当用工厂函数返回。
      // 工厂函数会收到组件所接收的原始 props
      // 作为参数
      default(rawProps) {
        // default 函数接收传入的原始 props 作为参数
        return { message: 'hello' }
      }
    },
    // 自定义类型校验函数
    propF: {
      validator(value) {
        // The value must match one of these strings
        return ['success', 'warning', 'danger'].includes(value)
      }
    },
    // 函数类型的默认值
    propG: {
      type: Function,
      // 不像对象或数组的默认,这不是一个工厂函数。这会是一个用来作为默认值的函数
      default() {
        return 'Default function'
      }
    }
  }
}
</script>


//组合式
<scipt setup>
defineProps({
  propA: Number,
  ...
})
</scipt>

子父 (emit)

通过自定义事件实现,给子组件绑定自定义事件,子组件触发自定义事件时传递,事件函数是父组件方法,父方法负责接收

<template>
	..
	<子 @自定义事件="父方法"></..>
	..
</template>

<script setup>
const 父方法 = (接受数据) => {处理}
</script>

<template>
	<button @click="$emit('自定义时间',子数据)">
    
  </button>
</template>

<script setup>
import { onMounted, ref } from 'vue';
const emit = defineEmits(['自定义事件'])
const 数据名 = ref(值)
onMounted(() => emit('自定义事件', 数据名))
</script>

爷孙 ($attrs透传)

假设A>B>C三个组件关系,A传递给C,通过中间的B,A作为祖先传递(属性绑定),C作为后代一定要接受(props),中间层所有的组件值负责做二传手的动作,如下

<中间层组件 v-bind="$attrs" ></..>

$attrs里面包含了所有上层组件传递过来的属性/事件

如果中间层组件有定义props对应的attrs会被当前组件消费掉

这里利用了,透传 Attribute,即组件嵌套的关系中,所有属性一路向子传递,子组件未定义props 或 emits 时的那一部分属性和事件都在$attrs中被保留

集中式($root)

把数据集中存到根组件,其他组件直接修改或者使用

app.vue

<!--组合式-->
<script setup>
import { ref } from 'vue';
const msg1 = ref('数据1')
//script setup 不会暴露内部声明的任何绑定 通过宏 显示公开
defineExpose({ msg1})
</script>

使用

<template>
  <h3>Chid</h3>
  <div>{{ $root.msg1 }}</div>
  <button @click="checkMsg1">修改msg1数据</button>
</template>

<!--组合式-->
<script setup>
import { getCurrentInstance, onMounted } from 'vue';
const instance = getCurrentInstance();//抓取当前组件实例
const checkMsg1 = () => {
  //instance.ctx.$root.msg1 = '修改后的数据' + Math.random()
  instance.root.exposeProxy.msg1 = '修改后的数据' + Math.random()
}
</script>

模板ref属性

引用元素(dom,组件),做dom操作,调用组件内部属性和方法

<!-- 父模板-->

<template>
  <子组件 ref="自定义子名称"></..>
  <div ref="自定义子名称"></div>
</template>

<!--组合式-->
<script setup>
import { onMounted, ref } from 'vue';
const 自定义子名称 = ref(null);
onMounted(() => {
  自定义子名称.value.数据名 //抓取子 的数据
  自定义子名称.value.方法() //调用了子的方法
  自定义子名称.value.dom操作
})
</script>

refs只会在组件渲染完成之后生效,并且它们不是响应式的,避免在模板或计算属性中访问refs 只会在组件渲染完成之后生效,并且它们不是响应式的,避免在模板或计算属性中访问 refs

组合式子组件内部要显式暴露 defineExpose({属性 })

scss/less/stylus

vue中使用样式预处理,样式预处理就是用js的逻辑书写css,之后由样式预处理转换成css

scss使用

vite下安装依赖

npm i sass sass-loader -D

组件件内使用

<style lang="scss">
$bg-color: red;
.title {
  color: $bg-color;
}
</style>

<template>
  <h3 class="title">app</h3>
</template>

全局scss变量

//vite.config.js
export default defineConfig({
  css: {
    preprocessorOptions: {
      scss: {
        additionalData: `@use "./src/assets/scss/theme.scss" as *;`,
      },
    },
  },
})

less使用

vite下安装依赖

npm i less less-loader -D

组件内使用

<style lang="less">
@width: 100px;
@height: @width + 100px;
#header {
  width: @width;
  height: @height;
  background: #ccc;
}
</style>

<template>
  <h3 id="header">ChildA</h3>
</template>

全局less变量

import path from "path";
export default defineConfig({
  preprocessorOptions: { 
    less: {
      javascriptEnabled: true,
      additionalData: `@import "${path.resolve(
        __dirname,
        "./src/styles/element/index.less"
      )}";`,
    },
  }
})

stylus使用

vite下安装依赖

npm i stylus -D

组件内使用

<style scoped lang="stylus">
/*变量 */
color=#399

.app
  color: color
  background: #ccc
</style>

全局stylus变量

export default defineConfig({
	css: {
    preprocessorOptions: {
      stylus: {
        imports: [path.resolve(__dirname, "src/styles/base.styl")],
      },

常用语法

后代包含

//stylus
.app
  background #ccc
  h3
    color red
//scss less
.app{
  background: #ccc;
  h3{
    color: red;
  }
}

编译为

.app{
  background: #ccc;
}
.app h3{
  color: red
}

父级引用

字符&指向父选择器。下面这个例子,我们两个选择器(textareainput)在:hover伪类选择器上都改变了color值:

//stylus
textarea,input
  color #A7A7A7
  &:hover
    color #000
//scss less
textarea,input{
  color: #A7A7A7;
  &:hover{
    color:#000
  }
}

编译为:

textarea,
input {
  color: #a7a7a7;
}
textarea:hover,
input:hover {
  color: #000;
}

混入

/* mixins.styl */
border-1px(color)
  position relative
	border none
	&:after 
		content ''
		position absolute
		bottom 0
		left 0
		background $color
		width 100%
		height 1px
		transform scaleY(0.5)
    
/* 组件内部使用*/
@import "./assets/mixins.styl";
.box
  border-1px(#ccc)
/*mixins.scss*/
@mixin border-1px($color) {
	position: relative;
	border: none;
	&:after {
		content: '';
		position: absolute;
		bottom: 0;
		left: 0;
		background: $color;
		width: 100%;
		height: 1px;
		transform: scaleY(0.5);
	}
}

/* 组件内部使用*/
@import "./mixins.scss";
.box{
	@include border-1px(#ccc);
}
/*mixins.less*/
.border-1px(@color) {
	position: relative;
	border: none;
	&:after {
		content: '';
		position: absolute;
		bottom: 0;
		left: 0;
		background: @color;
		width: 100%;
		height: 1px;
		transform: scaleY(0.5);
	}
}

/* 组件内部使用*/
@import "./mixins.less";
.box{
	.border-1px(#ccc);
}

编译为

.box{
  position: relative;
	border: none;
}
.box:after{
  content: '';
  position: absolute;
  bottom: 0;
  left: 0;
  background: #ccc;
  width: 100%;
  height: 1px;
  transform: scaleY(0.5);
}

hooks

  • 使用Vue3的组合API封装的可复用的功能函数

  • 自定义hook的作用类似于vue2中的mixin技术

  • 自定义Hook的优势: 很清楚复用功能代码的来源, 更清楚易懂

  • use开头的函数,要有返回值

    收集用户鼠标点击的页面坐标

// hooks/useMousePosition.js

import { ref, onMounted, onUnmounted } from 'vue'
/* 
收集用户鼠标点击的页面坐标
*/
export default function useMousePosition () {
  // 初始化坐标数据
  const x = ref(-1)
  const y = ref(-1)

  // 用于收集点击事件坐标的函数
  const updatePosition = (e) => {
    x.value = e.pageX
    y.value = e.pageY
  }

  // 挂载后绑定点击监听
  onMounted(() => {
    document.addEventListener('click', updatePosition)
  })

  // 卸载前解绑点击监听
  onUnmounted(() => {
    document.removeEventListener('click', updatePosition)
  })

  return {x, y}
}

使用hook

<template>
<div>
  <h2>x: {{x}}, y: {{y}}</h2>
</div>
</template>

<script setup>

import {
  ref
} from "vue"

import useMousePosition from './hooks/useMousePosition'
const {x, y} = useMousePosition()
</script>

封装发ajax请求的hook函数

//hooks/useRequest.js
import { ref } from "vue";
import axios from "axios";

export function useRequest(url, auto = true) {
  const data = ref(null);
  const loading = ref(false);
  const error = ref(null);

  function run() {
    loading.value = true;
    axios
      .get(url, { headers: { token: "1234567890123456" } })
      .then((res) => {
        loading.value = false;
        data.value = res.data;
      })
      .catch((e) => {
        loading.value = false;
        error.value = e.message || "未知错误";
      });
  }

  auto && run();

  return {
    loading,
    data,
    error,
    run,
  };
}

使用

<template>
  
  <h2 v-if="loading">LOADING...</h2>
  <h2 v-else-if="error">{{ error }}</h2>
  <button @click="run">按钮</button>
  <div>{{ data }}</div>
</template>

<script setup>
import { useRequest } from "./hooks/useRequest";
const { loading, error, data, run } = useRequest(
  "http://localhost:3000/api/home",
  false
);
</script>

<style></style>

hook库推荐

Composition API

所有api不影响源,只返回处理后的元素

to 转换

  • 当我们解构出ref或reactive数据内部数据时,会失去响应式,toRefs可以保持解构出数据的响应式
  • toRefs把一个响应式对象转换成普通对象,该普通对象的每个属性都是一个 ref
  • 当从hooks返回响应式对象时,toRefs 非常有用,这样消费组件就可以在不丢失响应式的情况下对返回的对象进行分解使用
<template>
  <h2>App</h2>
  <h3>foo: {{ foo }}</h3>
  <h3>bar: {{ bar }}</h3>
  <h3>foo2: {{ foo2 }}</h3>
  <h3>bar2: {{ bar2 }}</h3>
  <hr />
  <div>{{ a }}</div>
  <div>{{ b }}</div>
</template>

<script setup>
import { reactive, toRefs, ref } from "vue";

const data = reactive({
  foo: "a",
  bar: "b",
});

const { foo, bar } = toRefs(data);
setTimeout(() => {
  data.foo += "++";
  data.bar += "++";
  foo.value += "++";
  bar.value += "++";
}, 2000);

const data2 = ref({ a: 1, b: 2 });
let { a, b } = toRefs(data2.value);
setTimeout(() => {
  data2.value.a += "++";
  data2.value.b += "++";
  a.value += "++";
  b.value += "++";
}, 2000);

const { foo2, bar2 } = useXxx();
setTimeout(() => {
  foo2.value += "++";
  bar2.value += "++";
}, 2000);

function useXxx() {
  const data = reactive({
    foo2: "a",
    bar2: "b",
  });

  setTimeout(() => {
    data.foo2 += "++";
    data.bar2 += "++";
  }, 2000);

  return toRefs(data);
}
</script>

<style></style>

  • toRef为源响应式对象上的某个属性创建一个 ref对象, 二者内部操作的是同一个数据值, 更新时二者是同步的
  • toRef对比ref: ref拷贝了一份新的数据值单独操作, 更新时相互不影响
  • 应用: 当要将 某个prop 的 ref 传递给hook时,toRef 很有用
<template>
  <h2>App</h2>
  <p>data:{{ data }}</p>
  <p>foo:{{ foo }}</p>
  <p>foo2:{{ foo2 }}</p>

  <button @click="update">更新</button>

  <A :foo="foo"></A>
</template>

<script setup>
import { reactive, toRef, ref } from "vue";
import A from "./components/A.vue";

const data = reactive({
  foo: 1,
  bar: 2,
});

const foo = toRef(data, "foo");
const foo2 = ref(data.foo); //ref产生新引用

const update = () => {
  data.foo++;
  // foo.value++;
  // foo2.value++; // foo和data中的数据不会更新
};
</script>



<template>
  <h2>A</h2>
  <h3>{{ foo }}</h3>
  <h3>{{ props.foo }}</h3>
  <h3>{{ check }}</h3>
</template>

<script setup>
import { computed, toRef } from "vue";
const props = defineProps(["foo"]);

const check = useOddEven(toRef(props, "foo"));
// const check = useOddEven(props.foo);//传给hooks的不是响应式

function useOddEven(foo) {
  const check = computed(() => (foo.value % 2 === 0 ? "偶" : "奇"));
  return check;
}
</script>

shallow 浅层

浅响应式

  • shallowReactive : 只处理了对象内最外层属性的响应式(也就是浅响应式)
  • shallowRef: 只处理了value的响应式, 不进行对象的reactive处理
  • reactive与ref实现的是深度响应式, 而shallowReactiveshallowRef是浅响应式
  • 什么时候用浅响应式呢?
    • 一般情况下使用ref和reactive即可,如果考虑性能或者无效深度操作数据时,可以shallow一把
<template>
  <h2>shallow</h2>

  <h3>m1: {{ m1 }}</h3>
  <h3>m2: {{ m2 }}</h3>

  <button @click="update">更新</button>
</template>

<script setup>
import { shallowReactive, shallowRef } from "vue";

const m1 = shallowReactive({ a: 1, b: { c: 2 } });
const m2 = shallowRef({ a: 1, b: { c: 2 } });

const update = () => {
  m1.b = { c: 3 }; //有效
  m1.b.c += 1; //无效
  m2.value = { a: 11, b: { c: 22 } }; //有效
  m2.value.b.c += 1; //无效
};
</script>

read 可读性

控制可读性,可操作性普通和响应式数据

  • readonly:
    • 把响应式数据(ref,reactive),或者普通对象,包装成只读的,保持原有特性
    • 只读代理是深层的:访问的任何嵌套 property 也是只读的。
  • shallowReadonly
    • 浅只读包装
  • 应用场景:
    • 在某些特定情况下, 我们可能不希望对数据进行更新的操作, 那就可以包装生成一个只读代理对象来读取数据, 而不能修改或删除
<template>
  <h2>readonly</h2>
  <h3>{{ data }}</h3>
  <h3>{{ data2 }}</h3>
  <button @click="update">更新</button>
</template>

<script setup>
import { reactive, readonly, ref, shallowReadonly } from "vue";

const data = reactive({
  a: 1,
  b: {
    c: 2,
  },
});

const data2 = ref("bmw");

const rdata1 = readonly(data);
const rdata2 = shallowReadonly(data);
const rdata3 = shallowReadonly(data2);

const update = () => {
  // rdata1.a++; // error
  // rdata1.b.c++; // error
  // rdata2.a++; // error
  // rdata2.b.c++;
  rdata3.value = "bmw3";
};
</script>


Raw非响应

  • toRaw
    • reactivereadonly 的响应式去除,保留其他特性
    • 可用于临时读取,访问不会被代理/跟踪,写入时也不会触发界面更新。
  • markRaw
    • 把普通对象,标记为不接受响应式,即使存入响应式数据中,后期任然不响应
    • 应用场景:
      • 有些值不应被设置为响应式的,例如复杂的第三方类实例或 Vue 组件对象。
      • 当渲染具有不可变数据源的大列表时,跳过代理转换可以提高性能。
<template>
  <h2>{{ data }}</h2>
  <button @click="testToRaw">测试toRaw</button>
  <button @click="testMarkRaw">测试markRaw</button>
</template>

<script setup>
import { markRaw, reactive, ref, toRaw } from "vue";

// const data = reactive({
//   name: "tom",
//   age: 25,
//   likes: ["aa", "bb"],
// });

const data = ref({
  name: "alex",
  age: 19,
  likes: ["aa", "bb"],
});

const testToRaw = () => {
  console.log(1, data);
  const user = toRaw(data);
  console.log(2, user);
  // user.age++; // 界面不会更新
  user.value.age++; // 界面不会更新
};

const testMarkRaw = () => {
  const likes = { a: 1, b: 2 };
  // data.likes = likes;
  // data.value.likes = likes;
  // data.likes = markRaw(likes); // likes数组就不再是响应式的了
  data.value.likes = markRaw(likes); // likes数组就不再是响应式的了
  setTimeout(() => {
    // data.likes[0] += "--";
    // data.value.likes[0] += "--";
    data.value.likes.a += "--";
  }, 1000);
};
</script>

custom 自定义

  • customRef创建一个自定义的 ref,并对其依赖项跟踪,对更新触发进行显式控制
<template>
  <div>{{ count }}</div>
  <button @click="count++">+</button>
</template>

<script setup>
import { customRef, ref } from "vue";
// const count = ref(0);

const count = customRef((track, trigger) => {
  //业务
  let value = 0; //初始值
  return {
    get() {
      // 告诉Vue追踪数据
      track();
      return value; //返回初始值
    },
    set(newValue) {
      value = newValue; //更新
      // 告诉Vue去触发界面更新
      trigger();
    },
  };
});
</script>
  • 需求: 使用 customRef 实现 debounce 的示例
<template>
  <h2>App</h2>
  <input v-model="keyword" placeholder="搜索关键字"/>
  <p>{{keyword}}</p>
</template>

<script lang="ts">


import {
  ref,
  customRef
} from 'vue'

export default {

  setup () {
    const keyword = useDebouncedRef('', 500)
    console.log(keyword)
    return {
      keyword
    }
  },
}

/* 
实现函数防抖的自定义ref
*/
function useDebouncedRef(value, delay = 200) {
  let timeout: number
  return customRef((track, trigger) => {
    return {
      get() {
        // 告诉Vue追踪数据
        track()
        return value
      },
      set(newValue: T) {
        clearTimeout(timeout)
        timeout = setTimeout(() => {
          value = newValue
          // 告诉Vue去触发界面更新
          trigger()
        }, delay)
      }
    }
  })
}

</script>

is 判断

  • isRef: 检查一个值是否为一个 ref 对象

  • isReactive: 检查一个对象是否是由 reactive 创建的响应式代理

  • isReadonly: 检查一个对象是否是由 readonly 创建的只读代理

  • isProxy: 检查一个对象是否是由 reactive 或者 readonly 方法创建的代理

状态管理(Pinia)

打算开发中大型应用,集中式数据管理, 一处修改,多处使用,多个组件依赖于同一状态,来自不同组件的行为需要变更同一状态,生态环境给我们提供了官方插件Pinia

安装

# npm i pinia -S

// src/plugins/pinia.js
import { createPinia } from "pinia";
const pinia = createPinia();
export default pinia;

//main.js
import pinia from "./plugins/pinia";
app.use(pinia);

Pinia包成员

成员用途
createPinia创建一个 pinia实例
storeToRefs保证state解构后具备响应式
defineStore定义一个store,模块级的pinia
mapState函数,通讯工具 对象型
mapActions函数,通讯工具 对象型

Pinia实例相关成员

成员用途
state属性,获取所有状态(RefImpl),createPinia创建的根携带
use方法,安装插件,createPinia创建后的根携带

store实例相关成员

成员用途
$patch函数,批量修改
$state属性,当前store上的所有状态
$subscribe函数,订阅当前store上state的变化
$reset()函数, 重置状态到初始值,函数式store不可用

store的角色分工

component->actions->state<-getters->->component
发送修改状态请求同异步业务,修改状态存状态返回计算后状态渲染状态
学生代课老师财务班主任学生
$patch->key->()=>({key:value})<-key{{key}}
mapActions->
<-mapState
<-<-$state

定义store

定义一个store就是在定义一个公共状态和针对其增删改查的相关业务及对状态重新计算的一些逻辑,以备所有组件共享使用

// src/store/count.js
import { ref, computed } from "vue";
export const useCounterStoreFunction = defineStore("counterFunction", () => {
  const count = ref(10);
	
  function increment(value = 1) {
    count.value += value;
  }
  const double = computed(() => {
    return count.value * 2;
  });
  const asyncAdd = (payload) => {
    setTimeout(() => (count.value += 11), 1000);
  };

  return { count, increment, double, asyncAdd };
});

组件中实例化并使用

组合式组件+函数式pinia

<template>
  <div class="child">
    <div>{{ count }}/{{ double }}</div>
    <button @click="increment(2)">+</button>
    <button @click="$patch({ count: 3, bulala: 1 })">批量操作1</button>
    <button @click="patchs">批量操作2</button>
    <button @click="asyncAdd()">异步+</button>
    <button @click="modified()">替换state</button>
  </div>
</template>

<script setup>
import { useCounterStoreFunction } from "@/store/counter";
import { storeToRefs } from "pinia";
const counterFunction = useCounterStoreFunction();
const { count, double } = storeToRefs(counterFunction);//保持响应性
const { increment, asyncAdd, $patch } = counterFunction;

const patchs = () => {
  // counterFunction.$patch({ count: counterFunction.count + 1 }); //批量修改
  counterFunction.$patch((state) => {
    state.count++;
    state.bulala++;
  });
};

const modified = () => {
  counterFunction.$state = { count: 123 };
};

//订阅 counterFunction状态
counterFunction.$subscribe((mutation, state) => {
  // 每当它发生变化时,将整个状态持久化到本地存储
  localStorage.setItem("counterFunction", JSON.stringify(state));
});
</script>

第三方组件

使用一些别人开发好的组件 ,来写布局和功能,提高自己项目开发进度

组件库

pc端、后台管理

  • element- 饿了么 √
  • iview 个人
  • ant design 蚂蚁金服 √
  • naive Ui

移动端、客户端

  • vant 有赞 电商 √
  • mint-ui 饿了么
  • vue-material
  • muse-ui
  • VUX
  • cube-ui
  • vonic
  • Vue-Carbon
  • YDUI

通用

  • bootstrap5/4
  • ameizi

组件库

vant

官网 官网2

安装
npm i vant@3.4.9 -D

vite手动按需引入组件,样式自动导入

安装插件

npm i vite-plugin-style-import@1.4.1 -D

配置插件

安装完成后,在 vite.config.js 文件中配置插件:

import styleImport, { VantResolve } from 'vite-plugin-style-import';

export default {
  plugins: [
    styleImport({
      resolves: [VantResolve()],
    }),
  ],
};
使用

完成以上两步,就可以直接使用 Vant 组件了:

//main.js
import { createApp } from 'vue';
import { Button } from 'vant';
const app = createApp();

app.use(Button);
/*或者*/
app.component(Button.name, Button);

Vant 默认支持通过 Tree Shaking 实现 script 的按需引入。

自定义主题

main.js

//
import "./styles/vant/index.css";

./styles/vant/index.css

:root{
	--van-blue: #399;
}

所有基础变量链接

国际化

vant 采用中文作为默认语言,同时支持多语言切换,请按照下方教程进行国际化设置

main.js

import { Locale } from "vant";
// 引入英文语言包
import enUS from "vant/es/locale/lang/en-US";
Locale.use("en-US", enUS);
组件文档使用说明

Props

组件封装时内部可接受的props

<xx-xx :属性名="其他类型的值"></xx-xx>
<xx-xx 属性名="字符"></xx-xx>

方法

组件封装时,内部的method方法

<template>
	<xx-xx ref="引用名"
</template>
<script>
	this.$refs.引用名.方法()
  const 引用名 = ref(null)
  引用名.方法()
</script>

Events

组件封装时,内部可调用的自定义事件

<xx-xx @事件名="方法(参数)"

Slots

组件封装时,内部可接受的模板

<xx-xx>
	<template #槽名>..</template>
</xx-xx>

scope

作用域插槽向调用方传递的组件数据

<xx-xx>
	<template #槽名="当前组件收到的数据">..</template>
</xx-xx>

api options

组件库提供的方法可直接使用时,所传递的配置参数

<script setup>
import { 方法 } from 'vant'
方法({配置参数})
</script>

api 方法

组件库提供的方法可直接使用时,所传递的配置参数

<script setup>
import { 方法 } from 'vant'
方法.方法()
</script>

移动端适配

​ 视口,监听基础字号变化,单位转换(px2rem|viewport)

设置视口

<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover">

动态设定与修改基础字号

标准设计稿: 375px宽 / 基础字号37.5px/10rem满屏

方案: js

//uc-flexible.js
(function (doc, win) {
  var docEl = doc.documentElement,
   	w = 375, //标准
    resizeEvt = "orientationchange" in window ? "orientationchange" : "resize",
    recalc = function () {
      //控制基础字号
      var clientWidth = docEl.clientWidth; //取到宽度
      if (!clientWidth) return;

      // 设定最大支持范围, 淘宝的lib-flexible 个人的afme-flexible都有限定不支持pad或者更大尺寸
      // if (clientWidth >= w) {
      //   clientWidth = w;
      // }
			
      //标准 10倍
      docEl.style.fontSize = w/10 * (clientWidth / w) + "px";
    };

  //窗口变化时,文档加载完毕时,控制基础字号
  win.addEventListener(resizeEvt, recalc, false);
  doc.addEventListener("DOMContentLoaded", recalc, false);
})(document, window);

使用插件转换px

主要用到 PostCss后处理器,对css/scss/less等预处理器做后期打包、校验、添加前缀、转换等工作,剔除历史包袱

如果需要使用 rem 单位进行适配,推荐使用以下两个PostCss插件工具:

  • postcss-pxtorem 是一款 PostCSS 插件,用于将 px 单位转化为 rem 单位
  • amfe-flexible 用于设置 rem 基准值,可代替前面手动封装的,采用10倍布局,有最大尺寸限定

如果需要使用 viewport 单位 (vw, vh, vmin, vmax) 单位进行适配,推荐使用以下PostCss插件工具:

安装插件

#vp适配:
npm i postcss-px-to-viewport -D
# 或者
#rem适配: 
npm i postcss-pxtorem -D 针对标准10倍

配置

postcss.config.js

export default {
  plugins: {
    // rem
    "postcss-pxtorem": {
      rootValue: 37.5, //flexible计算的基础字号 375/10倍/字号37.5 
      rootValue({ file }) {
        return file.indexOf("vant") !== -1 ? 37.5 : 75; //vant375设计稿与实际设计稿750并存
      },
      propList: ["*"], // 需要转换的属性,这里选择全部都进行转换
    },
    
    //viewport
    'postcss-px-to-viewport': {
      viewportWidth: 375,//设计稿宽度 倍率依赖flexible
    },
  },
};

或者 vite.config.js

import postCssPxToRem from "postcss-pxtorem";
import postCssPxToViewport from "postcss-px-to-viewport";

export default defineConfig({
  css: {
    // 此代码为适配移动端px2rem
    postcss: {
      plugins: [
        postCssPxToRem({
          // rootValue: 37.5, // 转换倍率 1rem=37.5px
          rootValue({ file }) {
            return file.indexOf("vant") !== -1 ? 37.5 : 75; //vant375设计稿与实际设计稿750并存
          },
          propList: ["*"], // 需要转换的属性,这里选择全部都进行转换
        }),
        /* postCssPxToViewport({
          viewportWidth: 375,
          // exclude: [/src/],
        }), */
      ],
    },
  }
 
 .....

无论配置在哪,在main.js都需要引入flexible来监听基准尺寸

import 'amfe-flexible'import '自行封装的' //一般需要修改倍数时 或 突破受限

路由

用来SPA (single page application 单页面应用 )页面跳转,官网

单页VS多页

页面模式多页面模式(MPA Multi-page Application)单页面模式(SPA Single-page Application)
页面组成多个完整页面, 例如page1.html、page2.html等由一个初始页面和多个页面模块组成, 例如:index.html
公共文件加载跳转页面前后,js/css/img等公用文件重新加载js/css/img等公用文件只在加载初始页面时加载,更换页面内容前后无需重新加载
页面跳转/内容更新页面通过window.location.href = "./page2.html"跳转通过使用js方法,append/remove或者show/hide等方式来进行页面内容的更换
数据的传递可以使用路径携带数据传递的方式,例如:index.html?account="123"&password=123456"",或者localstorage、cookie等存储方式直接通过参数传递,或者全局变量的方式进行,因为都是在一个页面的脚本环境下
用户体验如果页面加载的文件相对较大(多),页面切换加载会很慢页面片段间切换较快,用户体验好,因为初次已经加载好相关文件。但是初次加载页面时需要调整优化,因为加载文件较多
场景适用于高度追求高度支持搜索引擎的应用高要求的体验度,追求界面流畅的应用
转场动画不容易实现容易实现

单页面模式:相对比较有优势,无论在用户体验还是页面切换的数据传递、页面切换动画,都可以有比较大的操作空间 多页面模式:比较适用于页面跳转较少,数据传递较少的项目中开发,否则使用cookie,localstorage进行数据传递,是一件很可怕而又不稳定的无奈选择

基础使用

安装

npm i vue-router -S

引入注册

// src/main.js
import { createApp } from "vue";
import router from "./plugins/router";
let app = createApp(App);
app.use(router);
app.mount("#app");

配置路由

// src/plugins/router.js

//1. 引入路由创建函数
import { createRouter, createWebHistory } from "vue-router";

//2. 路由配置
let routes = [
  {path: '/home',component: Home}, //route  一条路由的配置
]

//3.路由实例
let router = createRouter({ //插件路由对象
  // routes:routes
  routes,
  history: createWebHashHistory(), // 路由模式为必传项
});

//4.导出路由实例,让他去控制vue根
export default router

展示区

<router-view></router-view>
用来展示匹配到组件

声明式跳转

<router-link to="/home">声明式跳转</router-link>
<router-link to="/home">声明式跳转</router-link>
<router-link to="/home" active-class='css类名'>声明式跳转</router-link>

router-link 组件属性: active-class='类名' 指定激活后的样式 模糊匹配 exact-active-class='类名' 指定激活后的样式 严格匹配 router-link和router-view组件是vue-router插件提供的

重定向

//src/plugins/router.js  >  routers
{
  path: '/',  //默认页
  redirect: '/home' //配置型跳转
}, 

404

{
  //. 任何单字符,*表示零次或多次
  // :pathMatch(正则) 路径参数
  path: '/:pathMatch(.*)*',
  component: NoPage组件
}

路由嵌套

// src/plugins/router.js
routes=[
  {},
  {path:'xx/xxx',component:xx}, //在当前层级展示区展示
  {
    path:xx
    component:xx
    children:[  //子路由
      {path:'xxx', ..}
	    {.. ,redirect: 'xx'} //默认页
    ]
  },
  {}
]

动态路由

// src/plugins/router.js
routes=[
  {},
  {path:'xx/:id',component:xx},
  {
    path:xx
    component:xx
    children:[  //子路由
      {path:':id', ..}
    ]
  },
  {}
]

路由传参

// 组件中
<router-link to='xx/参数?a=1&b=2'></..>
<router-link :to='{name:'名字',params:{id:1},query:{a:2,b:3}}'></..>

命名路由

//src/plugins/router.js  =>  routes
{path: '/home',component: Home, name:'名字'}, //route  一条路由的配置

组件接参

//模板template
{{$route.params|query|path}} 


//script setup 组合型
import { useRoute } from "vue-router";
const route = useRoute();
route.query|params|..

编程式跳转


//script setup 组合型
import { useRouter } from "vue-router";
const router = useRouter();
router.push|replace|...

路由模式

// src/plugins/router.js

let router = new VueRouter({ //插件路由对象
  routes,
  // history:createWebHashHistory()//哈希模式   location.href
  history: createWebHistory(), //历史记录   history.pushState
});

扩展

路由守卫

全局守卫

// src/plugins/router.js

//前置
router.beforeEach((to, from, next) => {
  
  //	to: 目标路由 $route
  //	from: 当前路由 $route
  
  // next() 跳转  一定要调用
  next(false);//走不了
  next(true);//走你
  next('/login')//走哪
  next({path:'/detail/2',params:{},query:{}})//带点货
  
  // 守卫业务
  if(to.path=='/login' || to.path=='/reg' ){
    next()
  }else{
    if(是否登录){
      //axios请求 携带token
    }else{
    	next('/login')  
    }
  }
  
})

//后置
router.afterEach((to,from)=>{
  //全局后置守卫业务
})

路由独享守卫

// src/plugins/router.js -> routes
{
  path: '/user',
  component: User,
  beforeEnter: (to,from,next)=>{ //路由独享守卫 前置 
    console.log('路由独享守卫');
    if(Math.random()<.5){
      next()
    }else{
      next('/login')
    }
  }
 },

独享,没有后置

组件内部守卫

//script setup 组合型
import { onBeforeRouteLeave, onBeforeRouteUpdate  } from "vue-router";
onBeforeRouteUpdate((to, from, next) =>{
  // 在当前路由改变,但是该组件被复用时调用
  // 举例来说,对于一个带有动态参数的路径 /foo/:id,在 /foo/1 和 /foo/2 之间跳转的时候,
  // 由于会渲染同样的 Foo 组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。
}),
onBeforeRouteLeave((to, from, next) => {
  // 导航离开该组件的对应路由时调用
});

路由元信息

定义路由的时候配置 meta 字段

//src/plugins/router.js
{
  path: '/home',
  component: Home,
  meta: { requiresAuth: true, title:'标题内容' }
}

访问 meta 字段

//守卫时
to.meta from.meta

//script setup 组合式
const route = useRoute();
route.query|params|..

滚动行为

SPA是单页面,只有一个滚动条,路由跳转时滚动条会影响到元素位置,使用前端路由,当切换到新路由时,想要页面滚到顶部,或者是保持原先的滚动位置,就像重新加载页面那样

对于所有路由导航,简单地让页面滚动到顶部

// src/plugins/router.js
const router = createRouter({
  scrollBehavior (to, from, savedPosition) {
    //计算位置
    return { top: 0 }
  }
})

路由懒加载

让路由配置时所指向的组件,无需一开始就加载到app.js,而是分块到不同的js文件,在路由访问时加载对应组件(js),减少首屏压力,其原理是利用webpack对代码进行分割,异步调用组件,组件懒加载又叫异步路由、分片(块)打包、code splitting、异步组件

配置

// src/plugins/router.js

- import Home from 'xxx'
+ const home =()=>import("../pages/home.vue");

动态路由

可能想在应用程序已经运行的时候动态添加或删除路由,比如后台管理系统的不同权限账户登录后获取不同菜单

添加路由

//组合式
const router = useRouter();
router.addRoute({
  path: "/user",
  component: () => import("../pages/user.vue"),
})

删除路由

router.removeRoute('路由name')

添加嵌套路由

router.addRoute("路由name", {
  path: "sufei",
  component: () => import("../pages/sufei.vue"),
});

项目

技术栈选型

前端

vite

vue-router

axios

vant

pinia

全家桶

后端

方案1. 数据模拟json-server + mockjs

方案2. 使用第三方接口(网易云音乐、追书)

工作区目录

dist
  |-...	 //打包后,生产环境资源
public   //不优化 数据资源
  |-data:
    |-写死数据.json
    ....
  |- 图/字体/MP4
	|- index.html //浏览器入口
node_modules //第三方包
src  //开发环境 会优化  
  |-assets //全局写死资源 开发资源
    |-js
    |-css
    |-image
    |-...
  |-pages  //路由页面  不通用
    |-	home.vue / follow.vue / column.vue / user.vue
    |-  detail.vue / login.vue / reg.vue 
    |-	goods //目录 
      |- comment.vue
      |- goods
        |- boots.vue
      |- goods.vue
  |-layouts //布局组件
    |- App.vue
		|- error.vue
	|-api/services/http/request //服务 请求服务器接口相关的封装
		|- 
  |-utils //js工具包
    flexible.js
    ...
  |-plugins //插件配置
    axios
    router
  |-components //通用组件
    //对第三方组件做二次封装 / 自行封装的通用组件
		|- MyNavBar
  main.js //模块主入口
package.json //依赖管理

移动端适配

​ 基础样式,视口,基础字号,px转rem、vh/vw

基础样式

@charset "utf-8";
/* CSS layout */

/* 默认样式 */
html{overflow-y:scroll; overflow-x: hidden;}
body,div,ul,li,span,img,input,form{ margin:0; padding:0;}
body,p,h1,h2,h3,h4,h5,h6,ul,ol,dl,li,dd{ margin:0;}
ul,ol{padding:0;}

body{ font-size:12px;font-family:"冬青黑体简体中文","微软雅黑","宋体",Arial,sans-serif;}
a{ text-decoration:none; color:#666666; }
li{ list-style:none; }
img{ border:none 0;}
input{ outline:none; border:none; background:none;}
input,textarea,select{ margin:0; }					
input,textarea{ padding:0;}
input::-ms-clear{display:none;}


/* 浮动公共样式 */
.left{ float:left; _display:inline; }
.right{ float:right; _display:inline; }
.clear:after{ display:block; content:''; clear:both;}
.clear{ zoom:1;}

[^]:

设置视口

<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover">

动态设定与修改基础字号

标准设计稿: 375px宽 / 字号37.5px / 10rem满屏

头条设计稿: 640px宽 / 字号100px / 6.4rem满屏

方案: js / css媒体查询

//uc-flexible.js
(function (doc, win) {
  var docEl = doc.documentElement,
    w = 640, //头条设计稿实际宽度
   	//w = 375, //标准
    resizeEvt = "orientationchange" in window ? "orientationchange" : "resize",
    recalc = function () {
      //控制基础字号
      var clientWidth = docEl.clientWidth; //取到宽度
      if (!clientWidth) return;

      // 设定最大支持范围, 淘宝的lib-flexible 个人的afme-flexible都有限定不支持pad或者更大尺寸
      // if (clientWidth >= w) {
      //   clientWidth = w;
      // }
			
      //头条 百倍
      docEl.style.fontSize = 100 * (clientWidth / w) + "px";
      //标准 10倍
      //docEl.style.fontSize = w/10 * (clientWidth / w) + "px";
    };

  //窗口变化时,文档加载完毕时,控制基础字号
  win.addEventListener(resizeEvt, recalc, false);
  doc.addEventListener("DOMContentLoaded", recalc, false);
})(document, window);

使用插件转换px

主要用到 PostCss后处理器,对css/scss/less等预处理器做后期打包、校验、添加前缀、转换等工作,剔除历史包袱

如果需要使用 rem 单位进行适配,推荐使用以下两个PostCss插件工具:

  • postcss-pxtorem 是一款 PostCSS 插件,用于将 px 单位转化为 rem 单位
  • amfe-flexible 用于设置 rem 基准值,可代替前面手动封装的,采用10倍布局,有最大尺寸限定

如果需要使用 viewport 单位 (vw, vh, vmin, vmax) 单位进行适配,推荐使用以下PostCss插件工具:

安装插件

#vp适配:
npm i postcss-px-to-viewport -D
# 或者
#rem适配: 
npm i amfe-flexible postcss-pxtorem -D 针对标准10倍
npm i uc-flexible postcss-pxtorem -D 针对特殊 100倍

配置

postcss.config.js

export default {
  plugins: {
    // rem
    "postcss-pxtorem": {
      rootValue: 100, //
      propList: ["*"], // 需要转换的属性,这里选择全部都进行转换
       rootValue({ file }) {
          return file.indexOf("vant") !== -1 ? 58.5938 : 100;
        },
    },
    
    //viewport
    'postcss-px-to-viewport': {
      viewportWidth: 640,//设计稿宽度 倍率依赖flexible
    },
  },
};

或者 vite.config.js

import postCssPxToRem from "postcss-pxtorem";
import postCssPxToViewport from "postcss-px-to-viewport";

export default defineConfig({
  css: {
    // 此代码为适配移动端px2rem
    postcss: {
      plugins: [
        postCssPxToRem({
          // rootValue: 100, // 6.4rem满屏  640
          // rootValue: 37.5, // 10rem满屏  375
          //针对newApp 640宽100倍率,vant使用375宽设计稿,重新计算基础字号
          rootValue({ file }) {
            // return file.indexOf("vant") !== -1 ? 37.5 : 64; //针对 下载的amfe-flexible.js 375/37.5/10倍
            return file.indexOf("vant") !== -1 ? 58.5938 : 100; //针对 自行封装lib-flexible.js 640/100/100倍
          },
          propList: ["*"], // 需要转换的属性,这里选择全部都进行转换
        }),
        /* postCssPxToViewport({
          viewportWidth: 375,
          // exclude: [/src/],
        }), */
      ],
    },
  }
 
 .....

无论配置在哪,在main.js都需要引入flexible来监听基准尺寸

import 'amfe-flexible'import '自行封装的' //一般需要修改倍数时

引入资源

  • index.html引入 不优化 全局
  • main.js 引入 优化 全局
  • 组件 style标签引入css,会 优化 , 不加scoped全局,加了是私有

资源路径指向

相对路径:一般指向写死的资源(css/js/images)

绝对路径: 一般指向数据资源(服务器数据资源/public目录)

布局方案

  • 切图:需要设计稿,客户端开发时用到
  • UI库: 管理端开发时用到,常用的UI库(elementUI/mintUI/vantUI/antd...)
  • 模板移植: 老项目重构时用到

修改第三方组件样式方案

  1. 修改主题 √

  2. 使用组件提供的 props √√√

  3. 审查元素,查询相关样式,拷贝到给组件添加 class名中,重定义 + scoped

  4. 样式穿透,给组件添加class之后只影响组件根元素 √√ css解决: .a :deep() .b { /* ... */ } 深度选择器 Scss解决: .a{ :deep() .b{} }

    less解决: .a{ /deep/ .b{} }

客户端代理

// 项目根/vite.config.js
export default defineConfig({
  server: {
    port: 8080,
    // https: true,
    proxy: {
      "/api": {
        //目标代理本地服务器
        target: "http://localhost:9001",
        changeOrigin: true, //开启代理
        //别名替换
        // rewrite: (path) => path.replace(/^\/api/, ""),
        // ws: true, //socket协议开启
      },
      "/book": {
        //目标代理到线上服务器
        target: "https://api.zhuishushenqi.com",
        changeOrigin: true, //开启代理
      },
    },
  },
})

axios封装

import axios from "axios";
import router from "./router.js";
import { Toast } from "vant";
import Vue from "vue";

//设定基础请求地址
axios.defaults.baseURL = "/api";

//添加一个请求的拦截器
axios.interceptors.request.use(
  (config) => {
    //config 含有发出的请求的配置信息  ~~ axios({config})

    //显示loading
    Toast.loading({
      message: "加载中...",
      forbidClick: true,
      loadingType: "spinner",
    });

    // 每次都自动携带token
    // 每次都自动携带token
    let user = window.localStorage.getItem("user");
    user = user ? JSON.parse(user) : {};
    config.headers = { token: user.token };
    // config.headers = { token: "1234567890123456" };

    return config; // 撒手放出经过配置的请求
  },
  (error) => {
    // 发出了错误的请求,拦截
    return Promise.reject(error);
  }
);

// 添加一个响应的拦截器
axios.interceptors.response.use(
  (response) => {
    //response  ~~ axios请求后的res

    Toast.clear(); //关闭loading

    // 响应数据回来后,到达目标组件之前,做点事   res.status   res.data.err == 2
    //校验返回数据,token过期,路由跳转login,传递当前路由地址
    if (response.data.err == 2) {
      // console.log("2", router);
      Toast({
        message: response.data.msg,
        duration: 1000,
        forbidClick: true,
      });
      // this.$router~~~ new 出来的router实例
      router.replace({
        path: "/login",
        query: {
          redirect: router.currentRoute.fullPath, //传递当前的路由地址给登录,便于登录成功后跳回来
        },
      });
    }

    return response.data; //奔向组件
  },
  (error) => {
    Toast({
      message: "接口不存在",
      duration: 1000,
      forbidClick: true,
    });
    return Promise.reject(error);
  }
);

export default axios;

API接口管理

前端到后端的请求业务,集中到一处管理,封装成一个个异步函数,对外暴露,未来需要更改时,无需深入到业务组件内部

import axios from '../plugins/axios';

export const queryBanner = async params=>axios.get('/banner',{
  params: params || {_page:1,_limit:3}
});

export const queryDetail = async ({_id,collectionName})=>axios.get(`/${collectionName}/${_id}`);

export const queryReg = async ({username,password}) => {
	 	let params = new URLSearchParams();
    params.append('username',username);
    params.append('password',password);
    return axios({
      url:'/reg',
      method:'post',
      data:params
      // data:{username, password}
    })
};

用户介权

//登录
login-> 发送登录请求 返回token,种本地(cookie/localStorage/vuex)->跳转之前页面|user

//页面访问

axios抓取token到后端校验=>不通过跳转/通过返回数据
//或者
前置路由守卫抓取token携带到后端校验=>不通过跳转/通过next

//注销: 
删除 本地 token, 跳转登录

await直接写在script setup 内,会阻塞渲染,当前组件会被理解为异步组件,当前组件渲染时外层需要被suspense包裹、

<suspense>
  <router-view></router-view>
</suspense>

ref 和reactive 都是不可以直接赋值的

路由监听

属性检测 $route

//选项式
watch:{
	$route:{
    handler(nextValue,PrevValue){},
    immediate:true
  }
}

//组合式
import { watch,watchEffect} from "vue";
import { useRoute } from "vue-router";
const route = useRoute();

watch(
  () => route.path,
  () => {
    console.log(route.path);
  }
)

watchEffect(() => {
  route.path;
  console.log("watchEffect-count", route.path);
});

作业

班长整理: word|excel文档

张三 github地址: 客户端 | 服务端 | ppt 线上api文档 : 服务端在线api文档 阿里云上线地址 : zhangsan.top | 22.33.101.12:3001 录制项目介绍的视频 (制作ppt,使用了哪些技术栈,完成哪些业务,讲解每个页面如何使用,如何实现,开发过程中遇到的坑及如何解决)

=====扩展选学=====

动态组件

组件动态化(数据化),在不同组件之间进行动态切换,component自身不会渲染

<component :is="组件实例(变量)名"></component>

动态组件切换时候,会有挂载和卸载发生

切换的组件,需要引入+注册

选项型通过组件名切换,组合型通过实例名切换

把组件实例放到reactive/ref中,会有性能警告,is要的是组件本身无需代理,使用shallowRef 或者 markRaw 包裹组件实例,取消代理

缓存组件

有时候我们不希望组件被重新渲染影响使用体验;或者处于性能考虑,避免多次重复渲染降低性能。而是希望组件可以缓存下来,维持当前的状态。这时候就需要用到keep-alive组件。

keep-alive 包裹了目标组件,对目标组件缓存,后期不会触发卸载挂载,但会触发activated/deactivated

  • activated 活动组件 被缓存时起效果

  • deactivated 非活动组件

<keep-alive
  :include="/组件名|组件名2/"  加入一部分
	:exclude="['组件名','组件名2']"  排除一部分
	:max = "数字"   最多可缓存的组件数,一旦这个数字达到了,时间戳最早出现的被卸载(遗忘)
>
	..目标组件..
</keep-live> 

keep-alive 不给属性时,默认内部出现过得组件,都会被缓存,子集仍然需要keep-alive包裹

include/exclude 根据组件的 name 选项进行匹配,缓存的组件需要显式声明一个 name 选项

<!--组合型-->
<script>export default {name:''}</script>
<script setup></script>

缓存组件可以包裹自定义普通组件、component系统组件,router-view第三方组件

开启keep-alive 生命周期的变化

  • 初次进入时: onMounted> onActivated
  • 退出后触发 deactivated
  • 再次进入:
  • 只会触发 onActivated
  • 事件挂载的方法等,只执行一次的放在 onMounted中;组件每次进去执行的方法放在 onActivated中

异步组件

在大型项目中,我们可能需要拆分应用为更小的块,并仅在需要时再从服务器加载相关组件 , Vue 提供了一个 defineAsyncComponent 方法 , 父组件引用子组件 通过defineAsyncComponent加载异步配合import 函数模式便可以分包

import { defineAsyncComponent } from 'vue'
import ErrorComponent from './components/ErrorComponent.vue'
import LoadingComponent from './components/LoadingComponent.vue'

// 不带选项的异步组件
const asyncPage = defineAsyncComponent(() => import('./NextPage.vue'))

// 带选项的异步组件
const asyncPageWithOptions = defineAsyncComponent({
   // 加载函数
  loader: () => import('./Foo.vue'),
  
  suspensible:true//关闭suspensible内置组件配合,默认是需要而外的suspensible

  // 加载异步组件时使用的组件
  loadingComponent: LoadingComponent,
  // 展示加载组件前的延迟时间,默认为 200ms
  delay: 200,

  // 加载失败后展示的组件
  errorComponent: ErrorComponent,
  // 如果提供了一个 timeout 时间限制,并超时了
  // 也会显示这里配置的报错组件,默认值是:Infinity
  timeout: 3000
})

递归组件

原理跟我们写js递归是一样的 自己调用自己 通过一个条件来结束递归 否则导致内存泄漏

<!-- app.vue -->
<template>
	<递归组件 :data="数据"></递归组件>
</template>

<!-- 递归组件.vue -->
<template>
	<ul>
    <li :key="index" v-for="(item, index) in data">
      <!-- TreeItem 其实就是当前组件 通过import 把自身又引入了一遍 如果他没有children 了就结束 -->
      <递归组件 v-if="item.children?.length" :data="item.children"></递归组件>
    </li>
  </ul>
</template>

传送门组件

Teleport 是vue内置组件,是一种能够将我们的模板渲染至指定DOM节点,不受父级style、v-show等属性影响,但data、prop数据依旧能够共用的技术;类似于 React 的 Portal。

<template>
	<Teleport to="body">
    要传送的元素(dom/组件)
  </Teleport>
  <Teleport to=".class名">
      要传送的元素(dom/组件)
  </Teleport>
  <Teleport to="#id名">
      要传送的元素(dom/组件)
  </Teleport>
</template>

命名空间组件

可以使用带点的组件标记,例如 <Foo.Bar> 来引用嵌套在对象属性中的组件。这在需要从单个文件中导入多个组件的时候非常有用

<script setup>
import * as Form from './form-components'
</script>

<template>
  <Form.Input>
    <Form.Label>label</Form.Label>
  </Form.Input>
</template>

组件通讯详解

贴层传递

父子

props

父组件通过属性绑定,子组件通过选项props接收,props是响应式的,props完成单向下行绑定

父调子时传

<子 :自定义属性="父数据"></..>

子收

<div>
  {{自定义属性}}
</div>
<!--组合式-->
<script setup>
//defineProps 宏方法 无需引入 
const props = defineProps(['自定义属性'])
console.log(props.foo)
</script>
$parent
 //子模板: 
$parent.父数据

//子js: 
this.$parent.父数据

使用场景: 通用组件(紧耦合)

父更新,子更新,选项式

路由

params,query,props(boolean,object,function)

子父

emit

通过自定义事件实现,给子组件绑定自定义事件,子组件触发自定义事件时传递,事件函数是父组件方法,父方法负责接收

<template>
	..
	<子 @自定义事件="父方法"></..>
	..
</template>

<!--选项式-->
<script>
export default {
  methods:{
    父方法(接受数据){处理}
  }
}
</script>

<!--组合式-->
<script setup>
const 父方法 = (接受数据) => {处理}
</script>

<!--组合式-->
<script setup>
import { onMounted, ref } from 'vue';
const emit = defineEmits(['自定义事件'])
const 数据名 = ref(值)
onMounted(() => emit('自定义事件', 数据名))
</script>
模板ref

引用元素(组件),调用组件内部属性和方法

<!-- 父模板-->
<template>
  <子组件 ref="自定义子名称"></..>
</template>

<!--组合式-->
<script setup>
import { onMounted, ref } from 'vue';
const 自定义子名称 = ref(null);
onMounted(() => {
  自定义子名称.value.数据名 //抓取子 的数据
})
</script>

refs只会在组件渲染完成之后生效,并且它们不是响应式的,避免在模板或计算属性中访问refs 只会在组件渲染完成之后生效,并且它们不是响应式的,避免在模板或计算属性中访问 refs

组合式子组件内部要显式暴露 defineExpose({数据名 })

兄弟(中间人)

兄弟A->emit->中间人(父)->props->兄弟B

v-model(父<->子)

可形成父子互传,且互相绑定,不仅实现相互传值,还可实现给组件实现双绑

<!--父-->
<template>
  <div id="app">
    <h3>组件通讯详解-v-model</h3>
    <div>{{ ipt1 }}</div>
    <A v-model:ipt1="ipt1"></A>
  </div>
</template>

<!--组合式-->
<script setup>
import A from "./components/A.vue";
import { ref } from "vue";
const ipt1 = ref("bmw");
</script>

子组件

<template>
  <h3>A</h3>
  <div>{{ ipt1 }}</div>
  <input
    type="text"
    :value="ipt1"
    @input="$emit('update:ipt1', $event.target.value)"
  />

  <input type="text" v-model="ipt2" />
  <button @click="add">add</button>
</template>

<!--组合式-->
<script setup>
import { ref } from "vue";
const ipt2 = ref("qq");

defineProps(["ipt1"]);
const emits = defineEmits(["update:ipt1"]);

const add = () => {
  emits("update:ipt1", ipt2.value);
};
</script>

隔层传递

$attrs(透传)

假设A>B>C三个组件关系,A传递给C,越过中间的B,A作为祖先一定要传递(属性绑定),C作为后代一定要接受(props),中间层所有的组件值负责做二传手的动作,如下

<中间层组件 v-bind="$attrs" ></..>

$attrs里面包含了所有上层组件传递过来的属性/事件

如果中间层组件有定义props对应的attrs会被当前组件消费掉

provide/inject

祖先组件中通过provide来提供变量,然后在子孙组件中通过inject来注入变量

<!--祖先组件-->
<!--组合式-->
  <script setup>
  import A from "./components/A.vue";
  import { provide, ref } from "vue";
  const lang = ref("zh-N");
  provide("version", "1.7.11");
  provide("lang", lang);
  </script>

<!--后代组件-->
  <template>
    <div>{{ name }}<div>  
  </template>

   <!--组合式-->
  <script setup>
  import { inject } from "vue";
  const version = inject("version");
  let lang = inject("lang");
  setTimeout(() => (lang.value = "cn"), 2000);
  </script>

使用场景:为高阶插件/组件库提供用例

provide 和 inject 绑定并不是可响应的。这是刻意为之的。然而,如果你传入了一个可监听的对象,那么其对象的属性还是可响应的

公共事件总线

在大多数情况下不推荐使用全局事件总线的方式来实现组件通信,虽然比较简单粗暴,但是长久来说维护事件总线是一个大难题

# npm install --save mitt

//src/bus.js
import mitt from 'mitt'
const bus = mitt()
export default bus;

//组件内部
import bus from '...';
bus.emit('事件',数据) //发布
bus.on('事件',(data)=>{处理})	//订阅
bus.off('事件')	//取消订阅
bus.all.clear()

集中式管理

$root

把数据集中存到根组件,其他组件直接修改或者使用

app.vue

<!--组合式-->
<script setup>
import { ref } from 'vue';
const msg1 = ref('数据1')
//script setup 不会暴露内部声明的任何绑定 通过宏 显示公开
defineExpose({ msg1})
</script>

使用

<template>
  <h3>Chid</h3>
  <div>{{ $root.msg1 }}</div>
  <button @click="checkMsg1">修改msg1数据</button>
</template>

<!--组合式-->
<script setup>
import { getCurrentInstance, onMounted } from 'vue';
const instance = getCurrentInstance();//抓取当前组件实例
const checkMsg1 = () => {
  //instance.ctx.$root.msg1 = '修改后的数据' + Math.random()
  instance.root.exposeProxy.msg1 = '修改后的数据' + Math.random()
}
</script>

状态管理

在浏览器下层,应用上层,打造一个“全局变量”,利用vuex|pinia插件管理

永久与临时

永久存储

存库 , web/本地存储(localstroge,cookie),后端文件存储(writeFile)

临时存储

状态管理,公共总线(mitt),$root(vue)

数据可视化

安装

npm i echarts --save

引入

import * as echarts from 'echarts'

使用

//实例化
let instance=echarts.init(dom元素);
//渲染
instance.setOption(数据)
//API: 
instance.showLoading()/hideLoading()/ on('事件名',方法)

//事件:
instance.on('click', function (params) {
    // 控制台打印数据的名称
    console.log(params.name);
});

资料

地图引入

这里以百度地图为例,其他第三方地图库学习方法同理,登录官网

  • 注册百度账号
  • 申请成为百度开发者 注册 浏览器端
  • 获取服务密钥(ak mT8XXo4kIGkUfzFeInb0A6GvzS09WtNv)
  • 使用相关服务功能

引入库

//vue的index.html cdn加入 
<script type="text/javascript" src="https://api.map.baidu.com/api?v=1.0&type=webgl&ak=UBMP6QMGFzoEIgb45h5RO0XD6nZcHvYT"></script>

使用

//装地图的id要有,元素要有高
<div id="map"></div>

API 事例

动画

vue动画通过系统组件 transition 和 transition-group来介入,谁做动画,就用此组件就包着谁,可以包的元素有dom,组件。

vue不渲染只声明逻辑的系统元素 有 template,keep-alive,component,router-view。

transition-classes.f0f7b3c9.png

实现方案

  • css动画
    • css过渡动画transition,无跳变,关注打哪来1,到哪去4
    • css帧动画animation,有跳变,关注来了停哪1.5,到哪去3.5
    • animate.css 帧动画库 animation思想
  • js控制dom完成动画
    • 第三方的js动画库

transition

组件属性

<tansition
	name =  "动画名"
  mode="out-in|in-out"  前后场景进退次序

  enter-from-class = "类名"
  enter-active-class = "类名"
  enter-to-class = "类名"
  leave-from-class = "类名"
  leave-active-class = "类名"
  leave-to-class = "类名"
>
	...要做动画的元素...
</tansition>

样式

<style>
.动画名-enter-from{..}  入场前(打哪来)
.动画名-enter-active{..}  入场中
.动画名-enter-to{..} 入场后(来了停哪)
.动画名-leave-from{..} 离场前
.动画名-leave-active{..}  离场中
.动画名-leave-to{..} 离开场后(到哪去)
</style>

组件事件

<tansition
	@before-enter="方法(el)"   设置元素的 "enter-from" 状态
  @enter="方法(el, done)" 用这个来开始进入动画
  @after-enter="方法(el)" 当进入过渡完成时调用
 
  @before-leave="方法(el)"  leave 钩子之前调用
  @leave="方法(el, done)" 用这个来开始离开动画
  @after-leave="方法(el)" 在离开过渡完成
>
	...要做动画的元素...
</tansition>

transition-group

一组元素做动画,transition-group 包着一组元素 ,每个元素要有key ,无key部动画,其他的用法同transition