一、vue3语法
01.生命周期
步骤1:vue2与vue3的生命周期对比如下
步骤2:组合式api中,生命周期钩子需要导入函数,且可以多次定义
<script setup>
onMounted(() => {
console.log('第一次调用钩子')
})
onMounted(() => {
console.log('第二次调用钩子')
})
</script>
02.setup钩子
写法1:原生写法(了解)
<script>
export default {
setup() {
// 数据
const message = ref('this is message')
// 处理函数
const logMessage = () => {
console.log(message.value)
}
// 必须要return后才能使用
return {
message, logMessage
}
}
}
</script>
写法2:setup语法糖(掌握)
<script setup>
// 数据
const message = ref('this is message')
// 处理函数
const logMessage = () => {
console.log(message.value)
}
</script>
03.data
方法1:reactive函数(只能用于声明复杂数据类型)
// (1)按需导入reactive函数。
import { reactive } from "vue"
// (2)使用reactive函数,包装复杂数据类型。
const state = reactive({
count: 100
})
// (3)Js中通过【state.count】访问,HTML中通过【state.count】访问。
const setCount = ()=> {
state.count += 1
}
方法2:ref函数(可以是任意数据类型,推荐!)
// (1)按需导入
import { ref } from 'vue'
// (2)用ref函数包装数据。(底层其实还是reactive)
const count = ref(0)
// (3)在Js中需要【count.value】才能获取值。 在HTML中可以直接用{{ count }}。
console.log(count.value);
// (4)修改数据,需要通过【count.value】
const setCount = () => {
count.value++
}
04.computed
写法1:只读写法
// (1) 按需导入computed函数
import { ref, computed } from "vue";
// (2)computed是一个函数
const count = ref([1, 2, 3, 4, 5, 6, 7, 8, 9])
const computed_count = computed(()=> count.value.filter(item => item > 2) })
写法2:getter、setter写法
// (1) 按需导入computed函数
import { ref, computed } from "vue";
// (2) 跟vue2有点像,提供setter、getter写法,只不过需要用computed函数包一下。
const computed_count = computed({
get: () => {
return count.value + 1;
},
set: (val) => {
count.value = val - 1;
}
})
05.watch
可以直接监听响应式对象,无需.value访问。
情况1:监听一个简单数据类型。
// 按需导入watch函数
import { ref, watch } from "vue";
// 测试数据
const count = ref(1)
// (1) watch函数
// 参数1:响应式数据对象(无需.value)
// 参数2:回调函数
watch(count, (newValue, oldValue) => {
...
})
情况2:监听多个简单数据类型。
// 按需导入watch函数
import { ref, watch } from "vue";
// 测试数据
const count = ref(1);
const name = ref('思密达')
// (1) watch函数
// 参数1:响应式对象组成的数组。
// 参数2:函数。
watch([count, name], ([newCount, newName],[oldCount, oldName]) => {
...
})
情况3:监听整个对象。
import { ref, watch } from "vue";
// 测试数据
const obj = ref({
age: 18,
name: '张益达'
});
// (1) watch函数
// 参数1:要监控的对象本身;
// 参数2:函调函数;
// 参数3:配置对象。
watch(obj, (newValue, oldValue) => {
console.log(newValue, oldValue);
}, {
immediate: true, // 表示页面一打开就执行一次watch
deep: true // 深度监视(默认是浅层监视,只能监视简单数据类型)
})
情况4:监听对象中的某个属性。
import { ref, watch } from "vue";
// (1) 定义响应式数据obj
const obj = ref({
age: 18,
name: '张益达',
})
// (1) watch函数
// 参数1:回调函数;
// 参数2:函数;
watch(
() => obj.value.age, // 固定写法,以回调函数的形式。
(newValue, oldValue) => { ... }
)
06.获取DOM实例
使用ref对DOM节点进行标识,然后通过ref(null)函数来获取对应的DOM节点。
功能1:获取DOM节点实例
<script setup>
import { ref } from 'vue'
// 调用ref函数,根据ref标识得到ref对象
const h1Ref = ref(null)
</script>
<template>
<h1 ref='h1Ref'>我是h1标签</h1>
</template>
功能2:获取组件实例,使用演示如下:
A组件
<script setup>
// 2.定义响应式数据testMessage
const testMessage = ref('我真的很帅')
// 3. 将testMessage暴露出去(核心!)
defineExpose({
testMessage
})
</script>
<template>
<h1 ref='h1Ref'>我是h1标签</h1>
</template>
B组件
<script setup>
import A from '@/components/A.vue'
// 1.如果B组件想调用A组件内提供的testMessage数据,
// 需要在A组件中通过defineExpose({})暴露后才能被访问。
const ARef = ref(null)
console.log(ARef.testMessage.value)
</script>
<template>
<A ref='ARef'>我是h1标签</A>
</template>
07.自定义指令
步骤1:了解钩子函数(和组件的生命周期名称一致了。)
步骤2:去main.js文件中注册指令
const app = createApp(App)
app.directive('指令名称', {
// 钩子函数
})
08.给组件起名称
写法1:现在的写法(vue3.3以上的版本可用!)
<script setup>
defineOptions({
name: '组件名称'
})
</script>
写法2:过去的写法
<script>
export default {
name: '组件名称'
}
</script>
<script setup>
</script>
09.延迟执行JS
vue3的代码一般在setup中书写,没有this可用。
<script setup>
// 导入nextTick函数
import { nextTick, ref } from 'vue'
// 准备数据
let flag = ref(true)
// Mounted生命周期
onMounted(() => {
// 延迟到DOM渲染后,才执行该代码
nextTick(() => {
flag.vlaue = false
})
})
</script>
10.插槽
11.指令修饰符
第一类:v-model修饰符
(1).lazy(输入框聚焦、失焦时触发)
(2).number(尝试将输入框的value值转为number类型)
(3).trim(去除输入框的value值首尾空格)
第二类:事件修饰符
(1).stop(阻止事件冒泡)
(2).prevent(防止触发默认事件)
(3).capture(将默认触发方式改为捕获触发)
(4).self(只有当事件发生在目标元素自身时才会触发)
(5).once(事件只触发一次)
第三类:键盘修饰符
(1).enter(回车键触发)
(2).tab(tab键触发)
(3).delete(通过该元素需要配合v-if使用,是指DOM节点显示、隐藏时触发)
(4).esc(esc键触发)
二、项目起步
01.搭建vue3环境
vue-cli是基于webpack构建vue2、3项目。 create-vue是基于vite构建vue3项目。
1.安装node环境(必看)
这里使用nvm的方式安装node.js,要求:安装前必须卸载电脑上原有的node.js。
步骤1:安装nvm
网盘地址:'https://pan.baidu.com/s/1uoxlk8CVNHV2KTCwIGbQMQ?pwd=yi5m'
提取码:'yi5m'
步骤2:配置nvm镜像
// 在命令行依次执行下面的两条命令
'nvm npm_mirror https://npmmirror.com/mirrors/npm/'
'nvm node_mirror https://npmmirror.com/mirrors/node/'
步骤3:nvm命令
// 查看当前安装和使用的 node 版本
'nvm list'
// 安装某个 node 版本
'nvm install v版本号'
// 切换 node 版本
'nvm use v版本号'
// 设置默认版本
'nvm alias v12.22.12'
步骤4:配置npm镜像
// 将npm源设置为淘宝镜像
'npm config set registry https://registry.npmmirror.com'
// 获取当前npm源
'npm config get registry'
2.创建vue3项目(必看)
步骤1:确保安装了node.js
// 查看node.js的版本
'node --version'
步骤2:npm命令创建vue3项目
// 1.初始化vue3项目结构
'npm init vue@latest'
// 2.紧接着输入项目的名称
'项目名称'
// 3.最后根据提示执行命令
'npm install'
步骤3:启动vue3项目
'npm run dev'
02.模块配置
1.路由模块(router)
步骤1:安装路由模块
'npm i vue-router@4'
步骤2:创建路由实例,并导出
import { createRouter, createWebHistory } from 'vue-router'
// (1) 创建路由实例对象
// hash模式:createWebHashHistory(基地址)。
// history模式:createWebHistory(基地址)。
const router = createRouter({
// 【import.meta.env.BASE_URL】是vite.config.js中的环境变量。
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
redirect: '/home',
},
{
path: '/home',
component: () => import('@/views/home.vue')
},
{
path: '/about',
component: () => import('@/views/about.vue')
}
]
})
// (2) 导出路由实例
export default router
步骤3:在main.js中,导入路由实例对象,并挂载到vue
// 导入路由模块
import router from '@/router/index.js'
// 注册
app.use(router)
步骤4:获取路由对象
在vue3中是通过内置方法来获取路由对象的,因为vue3不存在this指针。
<script setup>
import { useRote, useRouter } from 'vue-router'
// 1.获取全部路由,相对于this.$router
const router = useRouter()
router.push('/path')
// 2.获取本次路由,相对于this.$route
const route = useRoute()
</script>
步骤5:配置路由守卫
本质:在路由跳转前,可以进行路由拦截。
方法1:全局路由守卫(默认全部拦截,不过可以用数组来指定哪些不需要拦截的。)
// 1. 配置前置路由守卫
/* (1)to:到哪里去,完整路由信息对象(路径,参数) 获取路径:to.path
(2)from:从哪里来,完整路由信息对象(路径,参数)
(3)next():默认直接放行,放行到to要去的路径。
next(路径):进行拦截,拦截到next里面配置的路径。
*/
const authUrls = ['/pay', '/myorder']
router.beforeEach((to, from, next) => {
if (!authUrls.includes(to.path)) {
next()
return
}
// 如果是权限页面,需要判断token,如果存在就放行,如果不存在就跳到登录页面
const token = store.getters.token
token ? next() : next('/login')
})
// 2. 后置路由守卫
router.afterEach(() => {
nProgress.done()
})
方法2:独享路由守卫(给单个路由配置路由守卫)
2.pinia模块
// Pinia的优势
1. 去掉了mutaitons,只需写在actions即可。
去掉了modules,每一个store都是一个独立的模块。
2. 提供组合式API写法:普通数据就是state,普通函数就是actions,computed就是getters,确实有点牛逼。
3. 可以配合TypeScript,提供可靠的类型推荐。
4. pinia可以直接修改state,可以不通过mutations来修改。
步骤1:安装pinia
'npm i pinia'
步骤2:创建pinia实例(在store/index.js文件)
// 导入createPinia函数,创建出pinia实例
import { createPinia } from 'pinia';
// 导入pinia持久化插件
import persist from 'pinia-plugin-persistedstate'
const pinia = createPinia()
pinia.use(persist)
export default pinia
步骤3:将pinia实例在main.js中注册
import App from './App.vue'
import { createApp } from 'vue'
// 导入pinia实例
import pinia from '@/store/index.js'
const app = createApp(App)
// 把pinia实例挂载到vue
app.use(pinia)
步骤4:一个文件就是一个pinia模块(不区分核心模块和普通模块,无需单独配置modules)
import { defineStore } from 'pinia';
import persist from 'pinia-plugin-persistedstate' // pinia持久化插件。
// 1. 组合式API写法。 优点:普通数据就是state,普通函数就是actions,computed就是getters,确实有点牛逼。
export const useUserStore = defineStore('user', () => {
// (1) 维护数据count
let count = ref(0)
const setCount = (value) => count.value += value
const getCount = () => count.value
const doubleCount = computed(() => count.value * 2)
// (2) 维护数据token
const token = ref('123abc456bdc')
const setToken = (value) => token.value += value
const getToken = () => token.value
// (3) 记得最后要导出这些state、actions、getters。
return {
count, doubleCount, setCount, getCount,
token, setToken, getToken
}
},
{
// 2. 开启pinia持久化插件
// 写法1:使用默认配置。
persist: true
// 写法2:自定义配置。
persist: {
// 默认是localStorage
storage: sessionStorage,
// 默认持久化全部,可指定好哪些state、getters、actions要被持久化
paths: ['count', 'token', 'setToken', 'getCount'],
// 默认是以仓库的唯一标识'user'作为键名,值就是全部被持久化的state、getters、actions。
key: 'hm-user'
}
})
步骤5:pinia持久化插件安装
安装插件:
npm i pinia-plugin-persistedstate --save-dev
步骤6:使用pinia
<script setup>
// 导入store的counter模块
import { useUserStore } from '@/store/counter'
// (1) 不要对state、getters进行解构,会丢失响应式。
const userStore = useUserStore()
// (2) 如果真的需要解构,需要特殊处理后才能恢复响应式:需要使用storeToRefs()函数包一下。
const { count } = storeToRefs(userStore)
// (3) 如果解构store中的函数,则不用任何处理,因为函数不需要响应式。
const { addCount, minusCount } = userStore
</script>
<template>
<div>
<div>我是爷爷</div>
<span>{{ userStore.count }} - {{ userStore.msg }} - {{ userStore.double }}</span>
<button @click="userStore.addCount">+</button>
</div>
</template>
3.组件库(eLement-Plus)
官方文档:https://element-plus.org/zh-CN/
组件库安装:npm i element-plus --save
步骤1:安装组件库
'npm i element-plus --save'
步骤2:导入组件库到项目中
方法1:全部导入(便捷,但不节省空间)
import { createApp } from 'vue'
import App from './App.vue'
// 导入Element-Plus的组件和样式
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
const app = createApp(App)
app.use(ElementPlus).mount('#app')
方法2:按需导入(每次都要手动import,但是节约空间)
步骤2.1:首先你需要安装unplugin-vue-components
和 unplugin-auto-import
这两款插件
'npm install -D unplugin-vue-components unplugin-auto-import'
步骤2.2:去vite.config.js中配置刚刚安装的两个插件
步骤2.3:可能并没有完全搞定,可能还需要在main.js导入组件库的css样式。
import 'element-plus/dist/index.css'
4.axios模块
步骤1:发送get、post请求
GET请求的数据放params,POST请求的数据放data。
写法1:完整写法
axios({
method: 'get',
url: 'https://www.baidu.com',
// POST请求的数据放这
data: {
loginId: 'abc',
loginPwd: '123123',
nickname: '棒棒鸡'
},
// GET请求的数据放这
params: {
loginId: 'abc',
loginPwd: '123123',
}
headers: {
...
},
timeout: 1000,
// 一般都是json类型数据
responseType: 'json'
})
写法2:简洁写法
// 1.发送get请求
await axios.get("https://www.baidu.com", {
params: {
loginId: "abc"
},
})
// 2.发送post请求
axios.post("https://www.biadu.com", {
loginId: 'abc',
loginPwd: '123123',
nickname: '棒棒鸡'
})
步骤2:配置请求、响应拦截器
需要用到axios官方提供的 axios.create() 方法来创建实例对象,然后配置请求响应拦截器。
import axios from 'axios'
// 1.创建axios实例
const instance = axios.create({
baseURL: '',
timeout: 10000
})
// 2.添加请求拦截器
instance.interceptors.request.use(function (config) {
const token = localStorage.getItem('token');
if(token){
config.headers.authorization = token;
}
return config; // 返回处理后的配置
})
// 3.添加响应拦截器
instance.interceptors.response.use(function (resp) {
return resp.data;
}, function (error) {
alert(error.message); // 弹出错误消息
})
// 4.统一get、post的传参方式
const http = {
get(url:any, params:any, config:any) {
return new Promise((resolve, reject) => {
instance.get(url, {params, ...config}).then((res) => {
resolve(res)
}).catch((error) => {
reject(error)
})
})
},
post(url:any, params:any, config:any) {
return new Promise((resolve, reject) => {
instance.post(url, params, config).then((res) => {
resolve(res)
}).catch((error) => {
reject(error)
})
})
}
}
// 导出
export default http
步骤3:axios实现原理(可选)
function MyAxios (config) {
return new Promise((resolve, reject) => {
// (1)获取原生XMLHttpRequest,发起ajax请求。
let xhr = new XMLHttpRequest();
// (2)判断有没有携带params请求参数,拼接到url后面去。
if (config.params) {
//(传入的是params对象,我们需要转成uname='张三'&age=18的形式,调用URLSearchParams对象.toString()方法)
const queryString = new URLSearchParams(config.params).toString()
config.url += `?${queryString}`
}
// (3)判断是GET还是POST请求,如果不传默认使用GET请求。
xhr.open(config.method || 'GET', config.url);
// (4)绑定loadend事件,根据响应状态码,来决定是执行reject()函数,还是resolve()函数。
xhr.addEventListener('loadend', ()=> {
if(xhr.status>=200 && xhr.status<300) {
// 成功,回调resolve()方法
resolve(JSON.parse(xhr.response))
} else {
// 失败,回调reject()方法
reject(new Error(xhr.response))
}
})
// (5)发起请求之前,先判断在请求体里面有没有数据,如果有就必须告知请求体数据的类型。
if (config.data) {
xhr.setRequestHeader('content-Type', "application/json");
xhr.send(JSON.stringify(data));
} else {
xhr.send();
}
}
************************************************************
// 使用时和axios一样使用即可。
myAxios({
method: 'POST'
url: "http://localhost:8080/",
params: {
pname: "辽宁省",
cname: "大连市"
}
}).then((resp) => {
}).catch((error) => {
});
03.开发的问题解决方案
1.vite中配置源码路径@
问题描述:一般来说创建的vue项目是没有配置@为源码路径的,需要我们手动配置。
步骤:在vite.config.js文件中,配置alias选项
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
const path = require('path');
export default defineConfig({
plugins: [vue()],
resolve: {
// 配置路径别名
alias: {
'@': path.resolve(__dirname, './src'),
}
}
})
2.安装自动引入插件
问题描述: 每个.vue文件中,都需要手动导入依赖,例如:import { ref, onMounted } from 'vue' 这种每个文件都需要做的事情,能不能有一种方法帮我们自动引入。
步骤1:安装自动引入插件
`npm i unplugin-auto-import -D`
步骤2:去vite.config.js中配置,在plugins中配置AutoImport
// (1)导入插件
import AutoImport from 'unplugin-auto-import/vite'
export default defineConfig({
plugins: [
vue(),
// (2) 注册插件
AutoImport({
imports: ['vue', 'vue-router']
})
]
})
3.解决require is not defined
需要解决的问题: 在vite环境下,就可能出现require is not defined的报错
步骤1:安装插件
yarn add -D vite-plugin-require-transform
or
npm i vite-plugin-require-transform --save-dev
步骤2:去vite.config.js中配置,配置plugins
import { defineConfig } from 'vite'
import requireTransform from 'vite-plugin-require-transform';
plugins: [
requireTransform({
fileRegex: /.js$|.vue$/
})
],
如果还是解决不了,就去vite.config.js中修改如下配置
// 不用require('path')引入,而是使用import导入
import path from 'path'
4.解决跨域问题
跨域:跨域是由于浏览器的同源策略导致的,其实后端是成功的响应了数据,但是浏览器为了安全策略,没有把数据交给前端。同源策略是指:协议+主机名+端口号 三者一样才满足同源策略,否则为异源。
代理解决跨域原理:前端请求到node代理的服务器,通过node代理服务器来向后端发起请求,就可以成功的发起请求并获取数据。因为这是服务器与服务器之间的通信,不存在同源策略的影响,同源策略只在浏览器中起作用。
总结:前端只能解决开发环境下的跨域问题,无法真正的解决跨域,还得靠后端解决。
方法1:配置代理解决跨域(仅开发阶段)
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
// 导入自动import插件
import AutoImport from 'unplugin-auto-import/vite'
export default defineConfig({
// 插件
plugins: [
vue(),
AutoImport({
imports: ['vue', 'vue-router']
})
],
// 代理
server: {
proxy: {
'/api': 'http://testapi.xuexiluxian.cn'
}
},
// 配置@符号
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
}
})
方法2:解决生产阶段跨域问题(需要后端解决好跨域问题)
根据环境不同,从而请求不同的基地址。
(1)获取环境变量:process.env.VUE_APP_变量名
(2)获取环境:process.env.NODE_ENV
const instance = axios.create({
baseURL: process.env.NODE_ENV === 'production' ? process.env.VUE_APP_BASE_URL : '',
timeout: 2000
})
5.配置环境变量
// 1. 在哪配置环境变量
在项目根目录下,配置两个文件(这两个文件与src目录同级):
(1) .env.development // 开发环境会被运行
(2) .env.production // 生产环境会被运行
// 2.怎么定义环境变量?
必须以'VUE_APP_'开头,来声明环境变量。
例如:声明一个baseURL: 'VUE_APP_BASE_URL = https://www.baidu.com'
// 3.怎么读取环境变量?
(1) 访问环境变量:'process.env.VUE_APP_变量名'。
(2) 判断是否开发环境:'process.env.NODE_ENV'。// production、development
04.业务功能
1.登录功能
1.账号密码登录
2.短信登录(滑块验证)
// 步骤如下:
1.用户输入手机号,点击发送验证码
(1)前端要进行正则校验手机号
(2)如果合法就弹出滑块组件。
2.展示滑块组件(插件):插件本身做的事
(1)发请求:
前端给后端传递:验证码 + 随机数uuid + 当前时间戳,
后端给前端响应:背景图 + 小滑块 + 加密的校验码(根据:验证码 + 随机数uuid + 当前时间戳)
(2)前端拿到数据后,要干的活:
把背景图、小滑块渲染到页面,
把后端给的'加密校验码'存储起来(存储到一个变量或者存储到本地都行)
(3)用户交互行为:
鼠标按下:没有跟后端交互。
鼠标滑动:没有跟后端交互。
鼠标松开:与后端交互,看(4)
(4)发请求:
前端给后端传递:小滑块的位置 + 之前后端给的加密校验码
后端给前端响应:提示成功或者失败 + 第二次加密的校验码(根据:滑块位置 + 第一次加密校验码)
3.前端调用短信验证码API
(1)发请求:
前端给后端传递:手机号 + 之前后端第二次加密的加密校验码
后端给前端响应:成功就发短信,失败就提示错误就行了
3.微信扫码登录
步骤1:需要哪些参数,可以找微信开放平台。
微信开放平台:微信开放平台
步骤2:思路分析
(1)后端已经把所需的参数提供给前端了。(参数看下图)
(2)前端调API接口,拿到全部参数,并动态的创建一个script标签来加载微信二维码,
通过new WxLogin({...}),然后把后端给的参数一一传进去,就会自动生成一个二维码
(3)注意一下回调地址的填写:前端要写一个回调地址,后端也要写一个回调地址,
微信开放平台也有一个回调地址,必须三者同时一致,登录成功后才能进行页面重定向。
步骤3:代码实现
// 代码实现:
<script setup lang="ts">
import { onMounted } from 'vue'
import { getLoginParamsApi } from '@/api/login.js'
// 总结:加载JS脚本、发请求拿到密钥、创建WxLogin对象并传入密钥,生成微信二维码图片。
onMounted(() => {
const scriptDom = document.createElement('script')
scriptDom.src = 'http://res.wx.qq.com/connect/zh_CN/htmledition/js/wxLogin.js'
document.body.appendChild(scriptDom)
// JS加载完成后要做的事:
scriptDom.onload = async () => {
// 发请求拿参数
const resp = await getLoginParamsApi()
const { appid, redirectUri, scope, state } = resp.data
console.log(resp)
// 填入参数,生成微信二维码
let obj = new WxLogin({
self_redirect: false,
id: 'wxLogin',
appid,
scope, // 网页直接写死即可:'snsapi_login'
redirect_uri: decodeURIComponent(redirectUri),
state
})
}
})
</script>
<template>
<div class="login-form">
<div id="wxLogin" class="qrcode">二维码</div>
</div>
</template>
<style lang="scss" scoped>
// 扫码登录
.login-form {
.qrcode {
text-align: center;
}
}
</style>
步骤4:自定义微信二维码样式
// 自己写一个css,转换程MD5,然后传入到参数href中。
.impowerBox .qrcode {width: 200px;}
.impowerBox .title {display: none;}
.impowerBox .info {width: 200px;}
.status_icon {display: none}
.impowerBox .status {text-align: center;}
2.
05.常用组件
1.消息提示组件
1.创建组件
(1)index.js
import { createApp, ref, watch } from 'vue'
import ToastComponent from './index.vue'
// 消息队列
const messageArr = ref([])
// 弹出消息提示框
const showMessage = (options) => {
// 动态生成一个消息提示框
const app = createApp(ToastComponent, options)
const EL = document.createDocumentFragment() // 虚拟节点
const VM = app.mount(EL) // 真实的节点
// 从组件接收一些东西进来用
const { setIsShowMessage, setNewTop, top, height } = VM
// 然后插入到body后面
document.body.appendChild(EL)
// 加入到消息队列。
messageArr.value.push(VM)
// 监视messageArr,刚调用方法时要执行一次,为了为弹框设置一次高度。
watch(messageArr, () => {
// 动态计算当前消息提示框的位置。当前VM的位置= (当前位置的index + 1) * top + 当前位置index * height
const currentIndex = messageArr.value.findIndex(item => item === VM)
const newTop = (currentIndex + 1) * top + currentIndex * height
setNewTop(newTop)
}, { immediate: true })
// 几秒后,消息提示框消失、从消息队列中移除、删除DOM节点
setTimeout(() => {
setIsShowMessage(false)
messageArr.value = messageArr.value.filter(item => item !== VM)
VM.$el.addEventListener('transitionend', () => {
app.unmount()
})
}, options.duration);
}
export default showMessage
(2)index.vue
<script setup>
// 导入
import { ref, toRefs, onBeforeMount, onMounted, computed } from 'vue'
// 外界拿数据
const props = defineProps({
type: {
type: String,
default: 'info'
},
content: {
type: String,
default: '这是一个消息提示内容'
}
})
// 定义映射数据
const types = {
success: 'success',
error: 'error',
warn: 'warn',
info: 'info'
}
const resultType = computed(() => {
console.log(types[props.type]);
return types[props.type]
})
// 1.控制显示(默认不显示)
const isShowMessage = ref(false)
let timer = null
const setIsShowMessage = (flag) => {
isShowMessage.value = flag
}
// 2.设置默认值
let top = ref(20) // 默认高度
const height = 40 // 弹框原始高度
// 设置top
const setNewTop = (newTop) => {
top.value = newTop
}
// 导出
defineExpose({
setIsShowMessage,
setNewTop,
height,
top: 20
})
</script>
<template>
<Transition name="slide-fade">
<div v-show="isShowMessage" class='message-container' :class="[resultType]"
:style="{top: top + 'px'}">
{{ content }}
</div>
</Transition>
</template>
<style lang='scss' scoped>
.message-container {
position: fixed;
z-index: 9999;
left: 50%;
transform: translate(-50%);
width: 360px;
height: 40px;
background-color: #edf2fc;
border-radius: 15px;
text-align: center;
line-height: 40px;
color: #91949a;
transition: all 0.3s;
// 不同样式
&.success {
color: #67c23a;
background-color: #f0f9eb;
}
&.error {
color: #f56c6c;
background-color: #fef3f3;
}
&.warn {
color: #e6a9a2;
background-color: #fdf6ec;
}
&.info {
color: #91949a;
background-color: #edf2fc;
}
}
.slide-fade-enter-active {
transition: all 0.3s ease-out;
}
.slide-fade-leave-active {
transition: all 0.3s cubic-bezier(1, 0.5, 0.8, 1);
}
.slide-fade-enter-from,
.slide-fade-leave-to {
transform: translateX(-50%) translateY(-20px);
opacity: 0;
}
</style>
2.使用说明文档
// 步骤1:导入核心函数showMessage
import showMessage from '@/components/Message/index.vue'
// 步骤2:绑定好点击事件
const show = () => {
showMessage({
type: 'success', // 类型可以有:success、error、info、warn
content: '这是一条提示消息',
duration: 3000
})
}
2.滑动校验组件
步骤1:创建滑动校验组件index.vue
<template>
<div class="slider-container" :style="{ width: w + 'px' }">
<div class="slider-canvas" :style="{ width: w + 'px', height: h + 'px' }">
<!-- 大图 -->
<canvas :width="w" :height="h" ref="canvas" />
<!-- 小图 -->
<canvas class="slider-block" :width="l" :height="smallHeight" ref="block" />
<!-- 结果遮罩层 -->
<div
:class="`result-mask ${resultMask.class}`"
:style="{ height: `${resultMask.height}px` }"
/>
<div class="btn" @click="refresh"></div>
</div>
<!-- 拖动的滑块内容 -->
<div class="slider-square">
<div class="box">
<div
class="slider-square-icon"
@mousedown="sliderDown"
:style="{ left: slideInfo.sliderLeft }"
></div>
<span>{{ slideInfo.sliderText }}</span>
</div>
</div>
</div>
</template>
<script>
import { ref, reactive, onMounted, watch, onDeactivated } from 'vue'
export default {
props: {
w: {
// 大图的宽度
type: Number,
default: 320,
},
h: {
// 大图的高度
type: Number,
default: 160,
},
l: {
// 小图的宽度/高度(正方形的宽度,实际宽度需要加上圆弧的宽度)
type: Number,
default: 40,
},
sliderText: {
// 滑块文字
type: String,
default: "拖动滑块向右>>",
},
bigImg: {
// 大图源
type: String,
default: "",
},
smImg: {
// 小图图源
type: String,
default: "",
},
r: {
// 小图的拼图半径
type: Number,
default: 0,
},
accuracy: {
// 拖动误差范围
type: Number,
default: 5,
},
posY: {
// Y坐标
type: Number,
default: 0,
},
posX: {
// X坐标
type: Number,
default: 0,
},
smallHeight: {
// 小图的宽度/高度(正方形的宽度,实际宽度需要加上圆弧的宽度)
type: Number,
default: 40,
}
},
setup (props, { emit }) {
const canvas = ref(null); // 大图ref
const canvasCtx = ref(null); // 大图canvas绘制容器
const block = ref(null); // 小图ref
const blockCtx = ref(null); // 小图canvas绘制容器
const { w, h, l, r, sliderText, posY, posX, accuracy, smallHeight } = props;
const slideInfo = reactive({
sliderLeft: 0, // 可拖动滑块的left
block_x: 0, // 拼图最终要滑动的位置x
block_y: 0, // 拼图位置的y
sw: l, // 拼图的实际宽度 小图宽度 + 2*半径 + 3(border)
sliderText, // 滑块提示文字
bigImg: "",
smImg: "",
});
const origin = reactive({
// 鼠标按下初始位置
orginX: 0,
originY: 0,
});
const resultMask = reactive({
// 结果提示背景色
class: "",
height: 0,
});
// 初始化canvas dom
const initDom = () => {
canvasCtx.value = canvas.value.getContext("2d");
blockCtx.value = block.value.getContext("2d");
};
// 初始化图片
const initImg = () => {
const bigImg = createImg(() => {
canvasCtx.value.drawImage(bigImg, 0, 0, w, h);
}, props.bigImg);
const smImg = createImg(() => {
blockCtx.value.drawImage(smImg, 0, 0, l, smallHeight);
}, props.smImg);
};
const createImg = (onload, url) => {
const img = document.createElement("img");
img.crossOrigin = "Anonymous";
img.onload = onload;
img.onerror = () => {
img.src = url;
};
img.src = url;
return img;
};
onMounted(() => {
initDom();
initImg();
bindEvents();
});
const slider = ref(false);
// 鼠标按下
const sliderDown = (e) => {
slider.value = true;
slideInfo.sliderText = "";
origin.orginX = e.pageX;
origin.originY = e.pageY;
};
// 鼠标移动
const sliderMove = (e) => {
if (slider.value) {
const moveX = e.pageX - origin.orginX;
const moveY = e.pageY - origin.originY;
if (moveX < 0 || moveX + l > w) return false;
slideInfo.sliderLeft = moveX + "px";
let blockLeft = ((w - slideInfo.sw) / (w - 40)) * moveX;
block.value.style.left = blockLeft + "px";
}
};
// 鼠标松开
const sliderUp = (e) => {
if (!slider.value) return false;
slider.value = false;
resultMask.height = h / 2;
const left = parseInt(block.value.style.left);
emit("sliderJudge", left);
};
// 将鼠标移动,鼠标松开事件绑定在dom上,如果鼠标移出滑块范围,还可以继续移动
const bindEvents = () => {
document.addEventListener("mousemove", sliderMove);
document.addEventListener("mouseup", sliderUp);
};
// 调用接口获取图片,异步,监听图片地址变动
watch(
() => props.bigImg,
(newVal) => {
reset(); // 重置图片状态
initImg(); // 重新加载图片
}
);
//刷新图片
const refresh = () => {
emit("refresh");
}
// 重置/刷新图片
const reset = () => {
canvasCtx.value.clearRect(0, 0, w, h);
blockCtx.value.clearRect(0, 0, l, smallHeight);
slideInfo.sliderText = sliderText;
resultMask.height = 0;
resultMask.class = "";
slideInfo.sliderLeft = 0;
block.value.style.left = 0;
};
// 组建销毁时,移除鼠标事件
onDeactivated(() => {
document.removeEventListener("mousemove", sliderMove);
document.removeEventListener("mouseup", sliderUp);
});
return {
canvas,
canvasCtx,
blockCtx,
block,
slideInfo,
resultMask,
refresh,
sliderDown,
sliderMove,
sliderUp,
reset,
initDom,
initImg
};
}
}
</script>
<style lang="scss" scoped>
.slider-container {
.slider-canvas {
position: relative;
.slider-block {
position: absolute;
top: 0;
left: 0;
}
.btn {
position: absolute;
right: 0;
top: 0;
width: 34px;
height: 34px;
cursor: pointer;
background-image: url('../../assets/images/light.png');
background-position: 34px 35px;
z-index: 999;
}
@keyframes result-show {
0% {
opacity: 0;
}
100% {
opacity: 0.7;
}
}
.result-mask {
position: absolute;
left: 0;
bottom: 0;
width: 100%;
opacity: 0.7;
&.success {
background-color: #52ccba;
animation: result-show 1s;
}
&.fail {
background-color: #f57a7a;
animation: result-show 1s;
}
}
}
}
.slider-square {
background-color: #52a352;
height: 40px;
text-align: center;
line-height: 40px;
border: 1px solid #ddd;
position: relative;
margin-top: 12px;
.slider-square-icon {
position: absolute;
top: 0;
left: 0;
height: 39px;
width: 39px;
background-color: #ee9016;
box-shadow: blanchedalmond 0px 0px 1px 2px;
cursor: pointer;
&:hover {
background-color: rgb(224, 147, 30);
}
}
span {
color: #fff;
}
}
</style>
步骤2:该组件使用说明
// 步骤1:导入组件
import SlideSplit from '@/components/SlideSplit/index.vue'
// 步骤2:template声明组件
<template>
<SlideSplit v-if="isShowSlideSplit" :w="slideInfo.bigWidth" :h="slideInfo.bigHeight"
:l="slideInfo.smallWidth" :bigImg="slideInfo.bigImageBase64" :smImg="slideInfo.smallImageBase64"
:r="slideInfo.smallWidth" :accuracy="slideInfo.accuracy" :smallHeight="slideInfo.smallHeight"
@refresh="handlerRequest" @sliderJudge="onJudge"></SlideSplit>
</template>
// 步骤3:js中准备数据
const slideInfo = ref({
bigWidth: 0,
bigHeight: 0,
bigImageBase64: '',
smallImageBase64: '',
smallWidth: 0,
smallHeight: 0,
accuracy: 8,
bigImage: null, // 为了ts类型不报错,下面属性均无效。
smallImage: null,
posX: null,
posY: null
})
3.虚拟滚动组件(等高)
步骤1:创建虚拟列表组件index.vue
<script setup>
// 导入
import { ref, toRefs, onBeforeMount, onMounted, computed, onUnmounted } from 'vue'
// 1.总数据、每条数据的高度
const props = defineProps({
items: { // 总数据
type: Array,
defualt: () => []
},
itemSize: { // 每条数据的高度
type: Number,
defualt: '60'
}
})
// 2.内层容器总高度
const totalHeight = computed(() => {
props.items.length * props.itemSize
})
// 3.对10w+条数据处理后的数据。我希望格式:{item: 原始数据,position:该数据所在的位置}
const pool = ref([])
const container = ref(null)
let start = ref(0)
let end = ref(0)
const scrollTop = ref(0) //滚动的高度
const setScrollTop = () => {
// (1) 滚动条滚动的位置
scrollTop.value = container.value.scrollTop
// (2) 计算首尾位置
start.value = Math.floor(scrollTop.value / props.itemSize)
end.value = Math.ceil((scrollTop.value + container.value.clientHeight) / props.itemSize)
// (3) 优化:首尾多显示十个,防止出现白屏
start.value -= 10
end.value += 10
if (start.value < 0) { start.value = 0 }
// (4) 给渲染池赋值
pool.value = props.items.slice(start.value, end.value).map((item, index) => ({
item: item,
position: (start.value * props.itemSize) + index * props.itemSize
}))
}
// 4.页面加载
onMounted(() => {
setScrollTop()
container.value.addEventListener('scroll', () => {
setScrollTop()
})
})
</script>
<template>
<!-- 外层容器 -->
<div class='recycle-scroller-container' ref="container">
<!-- 内层超长容器 -->
<ul class="recycle-scroller-wrapper" :style="{height: `${totalHeight}px`}">
<li class="recycle-scroller-item" v-for="item in pool" :key="item"
:style="{height: `${itemSize}px`, transform: `translateY(${item.position}px)`}">
<slot :item="item.item"></slot>
</li>
</ul>
</div>
</template>
<style lang='scss' scoped>
// 最外层容器
.recycle-scroller-container {
overflow: auto;
width: 100%;
height: 100%;
border: 1px solid black;
margin: 0 auto;
scrollbar-width: none; // 在火狐中,隐藏滚动条样式
-ms-overflow-style: none; // 在ie10+、Edge中隐藏滚动条样式
// 在webkit内核,隐藏滚动条样式
&::-webkit-scrollbar {
display: none;
}
// 内层超长容器
.recycle-scroller-wrapper {
position: relative;
.recycle-scroller-item {
position: absolute;
top: 0;
left: 0;
width: 100%;
border-bottom: 1px solid black;
}
}
}
</style>
步骤2:组件的使用说明
<script setup lang="ts">
import { onMounted, ref } from 'vue'
// (1) 导入组件
import listItem from '@/components/scroll/listItem.vue'
import Scroll from '@/components/scroll/index.vue'
// (2) 测试生成一万条数据
const items = ref([])
for (let i = 1; i <= 10000; i++) {
let id = i
let name = `征服者${i}`
const obj = {
id: i,
name: name
}
items.value.push(obj)
}
</script>
<template>
<!-- (3) 虚拟列表组件的宽高由外层容器main-container决定 -->
<!-- (4) 需要传入数据、虚拟列表每一个item的高度值 -->
<!-- (5) 虚拟组件的每一个item的内容可以用插槽定制 -->
<div class="main-container">
<Scroll :items="items" :itemSize="200" v-slot="{item}">
<listItem :item="item" />
</Scroll>
</div>
</template>
<style lang="scss" scoped>
// 虚拟组件的大小,由外层容器main-container决定(内部设置了width、height为100%)
.main-container {
width: 800px;
height: 600px;
margin: 100px auto;
}
</style>
三、打包优化
01.vue配置分包(异步组件)
在vue中,只有把组件配置成异步组件,该组件才会被打包成单独的JS文件。
场景1:一些首屏页面看不到的组件,可以做成异步组件。只有当用户滑到组件的位置时,
才加载该组件,可以提升首屏加载速度。
// 思路
(1) 首先,组件不要直接import导入。
(2) 而是在js中异步加载组件:const componentC = defineAsyncComponent(() => import('@/components/componentC.vue'))
(3) 在template中:用'v-if'控制不能让组件渲染出来。
(4) 然后,当滑动到异步组件的位置时,由于使用vueuse中的函数,其会自动让状态'targetIsVisible'
变成true,此时v-if就为true了,异步组件就可以加载出来了。
**************************************************************************
<template>
<div>
<componentA></componentA>
<componentB></componentB>
<!-- 1. 一开始不要让C组件显示,等页面滚动到C组件的位置时,targetIsVisible变量就会变成true -->
<!-- 2. 用Suspense包裹异步组件,数据加载出来后会显示#default的内容,还没加载出来的话显示#fallback的内容。 -->
<div ref="target">
<Suspense v-if="targetIsVisible">
<template #default>
<componentC></componentC>
</template>
<template #fallback> 加载中... </template>
</Suspense>
</div>
</div>
</template>
<script setup>
// 导入函数
import { defineAsyncComponent, ref } from 'vue'
import { useIntersectionObserver } from '@vueuse/core'
// 导入组件, 其中componentC组件需要异步加载
import componentA from '@/components/componentA.vue'
import componentB from '@/components/componentB.vue'
const componentC = defineAsyncComponent(() => import('@/components/componentC.vue'))
// 3. 调用vueuse的useIntersectionObserver()方法
const target = ref(null)
const targetIsVisible = ref(false)
const { stop } = useIntersectionObserver(
target,
([{ isIntersecting }]) => {
if (isIntersecting) {
targetIsVisible.value = isIntersecting
}
}
)
</script>
场景2:若网络请求的数据还没拿到,可以显示一些提示消息,让用户稍等。
<template>
<div>
<!-- 1. 需要用Suspense包裹异步组件,数据加载出来后会显示#default的内容,还没加载出来的话显示#fallback的内容。 -->
<Suspense>
<template #default>
<componentA></componentA>
</template>
<template #fallback> 加载中... </template>
</Suspense>
<componentB></componentB>
<componentC></componentC>
</div>
</template>
<script setup>
// 导入函数
import { defineAsyncComponent, ref } from 'vue'
import { useIntersectionObserver } from '@vueuse/core'
// 导入组件, 其中componentC组件需要异步加载
const componentA = defineAsyncComponent(() => import('@/components/componentA.vue'))
import componentB from '@/components/componentB.vue'
import componentC from '@/components/componentC.vue'
// 2. 调用vueuse的useIntersectionObserver()方法
const target = ref(null)
const targetIsVisible = ref(false)
const { stop } = useIntersectionObserver(
target,
([{ isIntersecting }]) => {
if (isIntersecting) {
targetIsVisible.value = isIntersecting
stop()
}
}
)
</script>
02.动态组件(类似tab栏切换)
<template>
<ul>
<li v-for="(item, index) in tabList" :key="item.name" @click="changeCom(index)">
{{ item.name }}
</li>
</ul>
<!-- 动态组件 -->
<keep-alive>
<component :is="currentComponent.com"></component>
</keep-alive>
</template>
<script setup>
import { ref, markRaw } from "vue"
// 导入组件
import componentA from '@/components/componentA.vue'
import componentB from '@/components/componentB.vue'
import componentC from '@/components/componentC.vue'
// 准备数据,markRaw只是为了让它不要浪费响应式,我们不需要使用响应式。
let tabList = ref([
{ name: 'A小伙子', com: markRaw(componentA) },
{ name: 'B小伙子', com: markRaw(componentB) },
{ name: 'C小伙子', com: markRaw(componentC) }
])
// 选中当前组件
const currentComponent = ref({
com: componentA
})
// 改变组件
const changeCom = (index) => {
currentComponent.value.com = tabList.value[index].com
console.log(currentComponent.value.com, index);
}
</script>
03.按需加载路由
问题描述:vue是单页面应用程序,所有页面打包到一个JS文件中。必然会导致首屏加载过慢的问题。
解决方案:把路由组件改为按需引入,只有跳转到该路由时,才会加载该路由对应的组件。
04.页面白屏问题
问题描述:当页面打开时,HTML还没加载完就会同时加载JS,由于JS加载需要一段时间,从而导致有那么一秒钟左右页面处于白屏状态
解决方案:使用defer属性,<script defer></script>。就会等HTML解析完才加载JS。
四.项目上线
五.面试题
1.HTML
2.CSS
3.JS
4.Vue
1.vue2和vue3的区别
(1)组件注册不同:vue3不需要进行组件的注册,只需导入组件即可使用。
(2)template不同:vue3支持定义多个根节点,而vue2只能有一个根节点。
(3)语法不同:vue3使用组合式API,vue2是选项式API。
(4)生命周期不同:
vue2 | vue3 |
setup | |
beforeCreate | |
created | |
beforeMount | onBeforeMount |
mounted | onMounted |
beforeUpdate | onBeforeUpdate |
updated | onUpdated |
beforeDestroy | onBeforeUnmount |
destroyed | onUnmounted |
(5)vue3新出了Suspense内置组件:Vue3 提供 Suspense 组件,允许程序在等待异步组件加载完成前渲染兜底的内容,如 loading ,使用户的体验更平滑。使用它,需在模板中声明,并包括两个命名插槽:default 和 fallback。Suspense 确保加载完异步内容时显示默认插槽,并将 fallback 插槽用作加载状态。
<tempalte>
<suspense>
<template #default>
<List />
</template>
<template #fallback>
<div>
Loading...
</div>
</template>
</suspense>
</template>
vue3新出了Teleport内置组件:Vue3 提供 Teleport 组件可将部分 DOM 移动到 Vue app 之外的位置。比如项目中常见的 Dialog 弹窗。
<button @click="dialogVisible = true">显示弹窗</button>
<teleport to="body">
<div class="dialog" v-if="dialogVisible">
我是弹窗,我直接移动到了body标签下 </div>
</teleport>
(6)响应式原理不同:vue2基于Object.defineProperty来对数据进行劫持,而vue3使用的是ES6提供的Proxy对象。(这是因为:在vue2的响应式无法对数组或者对象进行增删改进行监听,所以vue2只能单独的提供了this.$set()方法进行处理,但是vue3使用Proxy,就无需进行特殊处理。)
(7)diff算法不同:
(8)TypeScript的支持不同:vue3支持TypeScript的语法,vue2对TypeScript的支持非常差。
2.v-if和v-show的区别
v-if是控制DOM节点是否要渲染,而v-show是在DOM节点渲染的情况下,通过display来控制DOM节点的显示与隐藏,本质上v-show的开销更大,v-if的稳定性不好。
总结:v-if适用于不频繁切换的场景,而v-show适用于频繁切换的场景。
3.v-for和v-if的优先级
Vue2中v-for的优先级会更高,所以会先执行循环,再进行v-if判断,所以这样就会导致无论需不需展示这个元素,都会先遍历整个列表
但是在Vue 3中,v-for和v-if的指令不能再同时使用在同一个元素上。这是因为Vue 3引入了一项优化,即在编译阶段会根据v-if的条件进行静态分析,如果条件为假,则不会生成对应的虚拟节点,从而减少了不必要的渲染开销。而如果v-for和v-if同时存在,会导致静态分析变得困难,因此Vue 3禁止了这种用法。
总结:vue3不能将v-if和v-for写在同一个节点。
4.vue3的内置组件有哪些
新增了三个组件。
(1)vue3新出了Suspense内置组件:Vue3 提供 Suspense 组件,允许程序在等待异步组件加载完成前渲染兜底的内容,如 loading ,使用户的体验更平滑。使用它,需在模板中声明,并包括两个命名插槽:default 和 fallback。Suspense 确保加载完异步内容时显示默认插槽,并将 fallback 插槽用作加载状态。
<tempalte>
<suspense>
<template #default>
<List />
</template>
<template #fallback>
<div>
Loading...
</div>
</template>
</suspense>
</template>
(2)vue3新出了Teleport内置组件:Vue3 提供 Teleport 组件可将部分 DOM 移动到 Vue app 之外的位置。比如项目中常见的 Dialog 弹窗。
<button @click="dialogVisible = true">显示弹窗</button>
<teleport to="body">
<div class="dialog" v-if="dialogVisible">
我是弹窗,我直接移动到了body标签下 </div>
</teleport>
(3)vue3新出了Fragment :组件可以没有根标签,内部会将多个标签包含在一个 fragment 的虚拟元素中。这个不需要自己手写,vue内部帮我们做了。
5.vue的事件修饰符有哪些,所在位置
第一类:v-model修饰符
(1).lazy(输入框聚焦、失焦时触发)
(2).number(尝试将输入框的value值转为number类型)
(3).trim(去除输入框的value值首尾空格)
第二类:事件修饰符
(1).stop(阻止事件冒泡)
(2).prevent(防止触发默认事件)
(3).capture(将默认触发方式改为捕获触发)
(4).self(只有当事件发生在目标元素自身时才会触发)
(5).once(事件只触发一次)
第三类:键盘修饰符
(1).enter(回车键触发)
(2).tab(tab键触发)
(3).delete(通过该元素需要配合v-if使用,是指DOM节点显示、隐藏时触发)
(4).esc(esc键触发)
6.vue双向数据绑定原理
在vue2:vue2数据双向绑定是由数据劫持 + 发布-订阅的模式实现的,通过object.defineProperty()来劫持对象属性的getter和setter操作,在数据变动时发布消息给订阅者,触发响应的监听回调。
在vue3:vue3使用的Proxy去劫持数据 当我们的数据发生变动时,发布消息给订阅者,触发相应的监听回调
7.v-for中的key值的作用
key值要设置一个唯一性的id,是为了确保渲染DOM的时候不会出错。因为创建DOM节点是比较消耗性能的操作,vue为了避免创建新的DOM节点,当vue发现一个节点的key值相同时,就不会创建新的DOM节点。
例如:
<template>
<div class="root">
<div class="item" v-for="(item, index) in list" :key="index">
<h2>{{ item.text }}</h2>
</div>
<button @click="replaceData">替换数据</button>
</div>
</template>
<script>
export default {
data() {
return {
list: [
{text: '33333'},
{text: '66666'},
{text: '99999'},
]
}
},
methods: {
replaceData() {
this.list = [
{text: '22222'},
{text: '44444'},
{text: '88888'},
]
}
}
}
</script>
<style lang="scss">
.item{ background: orange;}
</style>
初始化的样式
正常情况下:我们点击"替换数据"按钮此时会变成这样,很正常对吧。
特殊情况:当手动删除'66666'这个元素的DOM节点时,然后点击"替换数据"按钮,此时会变成这样子,额。。。我的44444跑哪去了? 这是因为vue发现这一批key和上一批key是一样的,就没有重新创建DOM节点,只是把内容给替换了而已。
8.mutations和actions的区别
如果方法体中涉及到异步操作,该方法需要转发给actions进行处理;
如果方法体中涉及的都是同步操作,该方法可以提交给mutations进行处理。
9.vue3新增的状态管理工具
pinia
10.css的预处理器有哪些,在vue中如何使用
css预处理器有:less、scss
以less为例:先安装less、less-loader:npm i less less-loader,
然后在styles标签中声明lang='less':<style lang="less">
11.vue的路由传参和接参
传参:
第一种:可以通过路径传参,把参数以"?参数1=值1&参数2=值2"的形式,放在路径后面。
// 1.查询参数传参(两种写法)
(1)this.$router.push('/home?username=长官&age=18')
(2)this.$router.push({
path: '/home'
query: {
name: '张三',
age: 18
}
})
// 2.动态路由传参
this.$router.push({
path: '/home',
params: {
username: '思密达'
}
})
第二种:给路由起个名称name,通过路由的name来进行跳转,参数要看情况放。
// 1.查询参数传参
this.$router.push({
name: '路由名称',
query: {
username: '思密达',
age: 18
}
})
// 2.动态路由传参
this.$router.push({
name: '路由名称',
params: {
username: '思密达'
}
})
接收参数:
// 1.获取查询参数传参
this.$route.query.参数名
// 2.获取动态路由传参
this.$route.params.参数名
12.watch和computed的区别
computed:是对响应式数据处理后, 返回的新的响应式数据。(带有缓存功能)
watch:是对响应式数据进行监听,可以监听普通数据类型,也可以监听对象。
13.vite的优点和特点
优点:
(1)构建速度快:运行时动态的加载依赖,避免传统构建工具的静态打包和编译。
(2)更高效的热更新:实时更新代码并预览修改效果,无需重新编译整个项目。
(3)配置简单
缺点:
(1)由于是比较新的技术,可能导致一些老的浏览器不兼容。
14.vuex的五个状态分别是什么
分别是state、mutations、actions、getters、modules。
15.vue2和vue3中v-for循环有什么区别
(1)key的不同:vue2需要手动配置key,vue3默认情况下会自动推导出唯一的key值。
(2)v-if和v-for的优先级:在vue2中v-for优先级高,在vue3中v-if优先级高。
16.自定义组件的过程
在components或者views目录下,创建一个xxx.vue文件,然后在文件内定义内容
<script setup>
// 定义组件名称
defineOptions({
name: 'TestVue'
})
</script>
<template>
<div class="test-container"></div>
</template>
<style lang="less" scoped>
</style>
使用组件时,vue2需要注册后使用,vue3可以直接使用组件。
17.localSorage和session的区别
localStorage:
(1)不管存进去是什么类型,读取出来的永远是字符串
(2)存储大小问题:localStorage的存储容量根据不同浏览器的不同,结果就不一样,一般为4M。
18.如果localStorage超过最大容量怎么办
方法1:压缩字符串,仍然存进localStorage
缺点:多引用一个库。
优点:压缩。
参考文章:2. lz-string【JavaScript 字符串压缩库】 - 简书
方法2:使用其他存储方式,如indexedDB,我们不是直接用indexed DB,而是用封装的库localForage
缺点:兼容性不太好,操作繁琐。
优点:存进去的是什么类型,读出来的就是什么类型。并且可以是异步的。存储量远远大于localStorage。