创建vue应用
传统方式创建vue应用
-
下载vue.global.js单文件
-
script标签中引入js文件
-
script标签中挂载vue
-
从vue.global.js的Vue中引入“CreateApp”:
const {createApp} = Vue; -
配置一系列需要由vue托管的变量及函数:
const a = 123; function b(){} -
创建由Vue托管的app对象,将上面说到的变量和函数通过setup函数进行返回
const app = { setup(){ return {a,b} } } -
使用createApp挂载到某个标签下,此时该标签受vue统一管理
createApp(app).mount("#app")
vue.global.js:
var Vue = (function (exports) { ... ... exports.computed = computed$1; exports.createApp = createApp; exports.createBlock = createBlock; ... ... return exports; }({}));(1) 首先定义了一个匿名函数,函数接收exports参数
function(exports)(2) 通过
funtion(args){...}(params)的方式立即执行该函数-
在本例中,传入的是一个空对象
{},在匿名函数中,exports就是这个空对象:exports = {} -
函数中配置了该对象的各种属性方法,例如
exports.createApp = createApp; -
通过
return exports返回该对象
(3) 此时Vue即这个返回的exports对象,这个对象中包含一系列的属性方法,包括下面使用到的createApp
-
使用cli创建应用
- 现在使用vue@cli、vite、npm创建的都是3.0的,只不过具体的版本可能有些区别
方法1:npm
-
创建vue应用:
npm init vue@latest使用默认配置: npm init vue@latest -y 或 npm init vue@latest --yes
npm init 会生package.json文件,而npm install则会根据package.json下载软件包及依赖
- 检查当前的镜像源地址: npm config get registry
- 把镜像源的地址切换为国内的淘宝服务器: npm config set registry=registry.npm.taobao.org/
-
进入app路径
cd <your-project-name> -
更新依赖
npm install -
运行程序
npm run dev、npm run build
方法2: vue@cli
- 全局安装cli:
sudo npm install -g @vue/cli - 创建项目:
vue create 项目名称 - 之前较早时期,使用vue@cli创建的默认是Vue2的应用程序,还需要使用命令进行更新:
vue add vue-next
方法3: vite
- 创建vue应用
npm init vite-app 项目名称 - 更新依赖
npm install - 运行程序
npm run dev、npm run build
挂载应用
与单文件开发不同的是,脚手架创建的应用中,setup、app的创建、挂载标签是发生在不同文件中的。
目录结构:
package_name
node_modules : 下载的npm包
public : 公开资源
src : 业务代码部分
- assets : 资源文件,例如css样式文件、svg矢量图片文件等
- components : 组件,即组成vue应用程序的各个“xxx.vue”文件
- App.vue : 一般默认使用App.vue作为主入口文件,主要用于配置
- main.js : 管理和挂载app
index.html : 主页面
package.json : 描述依赖包和版本情况,以及自定义脚本
-
在src/components/xxx.vue中配置setup函数,配置<template>标签数据,其整体会作为一个组件返回
<script setup> // 配置组件参数(属性) defineProps({ msg: { type: String, required: true } }) </script> // 以template方式组合标签功能,形成一个组件 <template> <div class="greetings"> <h1 class="green">{{ msg }}</h1> <h3> You’ve successfully created a project with <a href="https://vitejs.dev/" target="_blank" rel="noopener">Vite</a> + <a href="https://vuejs.org/" target="_blank" rel="noopener">Vue 3</a>. </h3> </div> </template> // 配置样式 <style scoped> h1 { font-weight: 500; font-size: 2.6rem; top: -10px; } h3 { font-size: 1.2rem; } .greetings h1, .greetings h3 { text-align: center; } @media (min-width: 1024px) { .greetings h1, .greetings h3 { text-align: left; } } </style> -
在App.vue中加载各个组件,组合完成相关业务需求
<script setup> //引入HelloWorld组件,组件名称默认就是文件名称 import HelloWorld from './components/HelloWorld.vue' </script> // App.vue中也是以<template>标签的方式组合组件功能 <template> <header> // 其他标签功能 <img alt="Vue logo" class="logo" src="./assets/logo.svg" width="125" height="125" /> <div class="wrapper"> // 直接以组件名称作为标签,使用组件 <HelloWorld msg="You did it!" /> </div> </header> </template> // 样式 <style scoped> header { line-height: 1.5; } .logo { display: block; margin: 0 auto 2rem; } @media (min-width: 1024px) { header { display: flex; place-items: center; padding-right: calc(var(--section-gap) / 2); } .logo { margin: 0 2rem 0 0; } header .wrapper { display: flex; place-items: flex-start; flex-wrap: wrap; } } </style> -
index.html中配置页面整体骨架,并通过标签id方式明确要挂载vue组件的位置
<!DOCTYPE html> <html lang="en"> // 骨架标签 <head> <meta charset="UTF-8"> <link rel="icon" href="/favicon.ico"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Vite App</title> </head> <body> // 通过id明确要挂载的位置 <div id="app"></div> <script type="module" src="/src/main.js"></script> </body> </html> -
main.js中挂载app
不同与单文件中以
const app = {setup(){return {xxx}}}的方式创建app,cli方式中,每一个vue后缀的文件就是一个“app”,只不过这个app的setup是在“xxx.vue”文件的<script>标签中进行的。所以如果要修改setup返回的内容,需要到vue文件中进行操作。import { createApp } from 'vue' import App from './App.vue' import './assets/main.css' createApp(App).mount('#app')
vue语法
基础
{{}}
{{}}用于数据绑定,使用setup return返回变量,在标签中通过{{变量名}}直接使用该变量
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/html" xmlns:v-bind="http://www.w3.org/1999/xhtml"
xmlns:v-on="http://www.w3.org/1999/xhtml">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="vue.global.js"></script>
</head>
<body>
<div id="app">
<div>
{{message}}
</div>
<div>
{{message + ': 数据绑定、表达式赋值测试'}}
</div>
</div>
</body>
<script>
const message = "vue功能测试";
const {createApp} = Vue;
const app = {
setup() {
return {
message,
}
}
}
createApp(app).mount("#app")
</script>
<style>
</style>
</html>
v-text和v-html
v-text用于配置文字内容,v-html可以将内容解析成html标签
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/html" xmlns:v-bind="http://www.w3.org/1999/xhtml"
xmlns:v-on="http://www.w3.org/1999/xhtml">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="vue.global.js"></script>
</head>
<body>
<div id="app">
<div v-text="message + ':v-text指令'"></div>
<div v-html="fragment"></div>
</div>
</body>
<script>
const message = "vue功能测试";
const fragment = "<div style='background-color: aqua'>vue功能测试:v-html指令</div>";
const {createApp} = Vue;
const app = {
setup() {
return {
message,
fragment
}
}
}
createApp(app).mount("#app")
</script>
<style>
</style>
</html>
v-bind和":"
v-bind用于绑定属性数据,可以缩写为:
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/html" xmlns:v-bind="http://www.w3.org/1999/xhtml"
xmlns:v-on="http://www.w3.org/1999/xhtml">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="vue.global.js"></script>
</head>
<body>
<div id="app">
<input v-bind:value="inputData" style="width: 100%"/>
<input :value="inputData + ' | 命令缩写'" style="width: 100%">
</div>
</body>
<script>
const inputData = "vue功能测试:v-bind属性绑定";
const {createApp} = Vue;
const app = {
setup() {
return {
inputData,
}
}
}
createApp(app).mount("#app")
</script>
<style>
</style>
</html>
[]
[]可以用变量值来表示属性名称
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/html" xmlns:v-bind="http://www.w3.org/1999/xhtml"
xmlns:v-on="http://www.w3.org/1999/xhtml">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="vue.global.js"></script>
</head>
<body>
<div id="app">
<input :[attr]="属性名动态改变'" style="width: 100%">
</div>
</body>
<script>
const attr = 'value';
const {createApp} = Vue;
const app = {
setup() {
return {
attr,
}
}
}
createApp(app).mount("#app")
</script>
<style>
</style>
</html>
v-on和"@"
v-on用于绑定事件监听函数,可以缩写为:,具体可以绑定什么事件,要根据标签来确定
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/html" xmlns:v-bind="http://www.w3.org/1999/xhtml"
xmlns:v-on="http://www.w3.org/1999/xhtml">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="vue.global.js"></script>
</head>
<body>
<div id="app">
<div>
<button v-on:click="onClick1">按钮1</button>
<button @click="onClick2">按钮2</button>
</div>
</div>
</body>
<script>
const {createApp} = Vue;
function onClick1() {
alert("vue功能测试:v-on事件监听")
}
function onClick2() {
alert("vue功能测试:v-on事件监听 | 缩写")
}
const app = {
setup() {
return {
onClick1,
onClick2,
}
}
}
createApp(app).mount("#app")
</script>
<style>
</style>
</html>
条件渲染
使用v-if、v-else-if、v-else做条件渲染
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/html" xmlns:v-bind="http://www.w3.org/1999/xhtml"
xmlns:v-on="http://www.w3.org/1999/xhtml">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="vue.global.js"></script>
</head>
<body>
<div id="app">
<div>
<div v-if="typ === 0">vue功能测试:条件渲染 - typ = 0</div>
<div v-else-if="typ === 1">vue功能测试:条件渲染 - typ = 1</div>
<div v-else-if="typ === 2">vue功能测试:条件渲染 - typ = 2</div>
<div v-else="typ === 3">vue功能测试:条件渲染 - typ = 3</div>
</div>
</div>
</body>
<script>
// 随机生成0~3的数字,以测试条件渲染效果
const typ = Math.floor(Math.random() * 10) % 4;
const {createApp} = Vue;
const app = {
setup() {
return {
typ,
}
}
}
createApp(app).mount("#app")
</script>
<style>
</style>
</html>
v-show
v-show也是起到根据需要显示或不显示元素内容,但是与条件渲染不一样的是,v-show是一定会被渲染出来的,只不过是不在页面上显示出来
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/html" xmlns:v-bind="http://www.w3.org/1999/xhtml"
xmlns:v-on="http://www.w3.org/1999/xhtml">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="vue.global.js"></script>
</head>
<body>
<div id="app">
<!-- v-show会被渲染,但是会根据属性值判断是否显示-->
<div v-show="typ === 1 || typ === 0"> vue功能测试:v-show</div>
</div>
</body>
<script>
// 随机生成0~4的值,以测试v-show效果
const typ = Math.floor(Math.random() * 10) % 4;
const {createApp} = Vue;
const app = {
setup() {
return {
typ,
}
}
}
createApp(app).mount("#app")
</script>
<style>
</style>
</html>
v-for
v-for多用于列表等标签的循环生成
对于列表list来说,存在索引和值两个数据内容,通过(item,index) in listxxx获取数据
对于字典map来说,存在键、值、索引三个数据内容,通过(value,key,index) in mapxxx获取数据
注意:
in可以替换成of,即(item,index) of listxxx或(value,key,index) of mapxxx- 小括号里的顺序是固定的,可以替换为任何内容,例如
(index,item) in listxxx中index表示的依然是元素内容,item表示的依然是元素索引。 - 也可以使用
item in xxx来处理,也就是将小括号的全部内容用item来代替
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/html" xmlns:v-bind="http://www.w3.org/1999/xhtml"
xmlns:v-on="http://www.w3.org/1999/xhtml">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="vue.global.js"></script>
</head>
<body>
<div id="app">
<div>
<ul>
<!-- 可以只写一个item: item in list1-->
<!-- item和index顺序是固定的: (元素内容, 元素索引)-->
<!-- item和index可以替换为其他名称: 例如(i,j)-->
<!-- in可以换成of: (item,index) of list1-->
<li v-for="(item,index) in list1">{{index}} : {{item}}</li>
</ul>
</div>
<div>
<ul>
<li v-for="(value, key, index) in map1">{{index}}: {{key}} = {{value}}</li>
</ul>
</div>
</div>
</body>
<script>
const list1 = ['aaa', 'bbb', 'ccc', 'ddd'];
const map1 = {
'name': 'aaa',
'id': 111,
'age': 18,
'gender': 'male'
}
const {createApp} = Vue;
const app = {
setup() {
return {
list1,
map1
}
}
}
createApp(app).mount("#app")
</script>
<style>
</style>
</html>
双向数据绑定
概念解析
双向数据绑定中,双向指的是数据流向可以由js到html,也可以由html流向js,双向数据绑定指的就是js和html中数据的变化可以互相影响。
- 传统原生js方式下,js修改html,需要通过document.getElementByxxx来获取元素,再修改元素来实现。而html修改js变量也是一样的,需要通过document.getElementByxxx来获取元素,再赋值给js变量
- 以小程序为例的单向数据绑定中,js修改html,可以通过
{{}}在html中绑定js变量。但是反过来,当html中的数据发生修改后,js是无法实时获取到的,需要通过event事件的方式,监听数据改变,再将修改后的数据赋给js变量
实现双向数据绑定
例如:
在vue中,返回了一个变量test
<script>
const test = 123;
const {createApp} = Vue;
function onclick() { alert(text); }
const app = {
setup() {
return {
test,
onclick
}
}
}
createApp(app).mount("#app")
</script>
此时在html中,使用之前说到的v-bind可以将变量绑定到标签中,但是此时依然是单向的,是无法反过来由html影响js的。无论在input中如何修改test的值,点击提交按钮后,依然显示为123
<div>
<input :value="test" type="text"></input>
<button @click="onclick">提交</button>
</div>
-
使用v-model替代v-bind,v-model其实相当于是v-bind+v-on,即包括了js绑定数据到html,以及以事件监听的方式监听html中的数据修改。
但此时依然是有问题的,无论在input中如何修改test的值,点击提交按钮后,依然显示为123
<div> <input v-model="test" type="text"></input> <button @click="onclick">提交</button> </div> -
将test由最初的普通变量修改为使用
ref报包装的变量,这个包装后的变量即为“响应式变量”<script> const {createApp,ref} = Vue; const test = ref(123); function onclick() { alert(text.value); } const app = { setup() { return { test, onclick } } } createApp(app).mount("#app") </script>同时,如果要获取这个变量的值,需要用value方法,因此还要修改click方法
(但是在v-model中是不需要使用
text.value这种形式的,vue会自动进行结构处理)function onclick() { alert(text.value); }此时即实现了双向的数据绑定。
分析
通过上述示例可以看出,要完成双向数据绑定,需要v-model+响应式对象。其中v-model相当于"v-bind+v-on",此外还需要一个响应式变量,这样才能实现js和html数据的双向流动。
-
v-model是有限制的,并不是所有的标签都可以使用v-model,一般常见于select、typearea、input等输入交互标签 -
v-model不是必须的,双向数据绑定的核心是“响应式对象”。v-model可以通过v-bind+v-on的方式替代,因为是响应式对象,当数据发生改变时会自动触发v-on监听函数,在这个函数中通过event.target.value就能获取到数据了 -
响应式对象除了
ref包装外,还可以使用reactive。一般来说,ref多用于普通变量的封装,而reactive多用于对象的封装。 -
其实在没有使用
ref将其包装为响应式对象时,这个变量既无法完成双向数据绑定,也无法完成单向数据绑定,只是在初始时将js变量显示在了html中。例如以下示例,无论+1按钮点击多少次,div中显示的依然是0
<div> <div>{{num}}</div> <button @click="onclick">+1</button> </div> <script> const {createApp} = Vue; const num = 0; function onclick() { console.log(num); } const app = { setup() { return { num, onclick } } } createApp(app).mount("#app") </script>
注意
被包装的这两个对象本身是不可修改的
ref:
const a = ref(123)
console.log(a)
a = 123
console.log(a)
reactive
const b = reactive({"val":123})
console.log(b)
b = {"val":123}
console.log(b)
watch监听函数
双向数据绑定可以实现数据的双向流动,数据发生修改时,可以通过watch来修改触发监听函数
q: 为什么有了响应式对象,能获取到对象的最新值,还要使用watch?
a: 这是因为对象数据改变是实时的,但是这个时间我们却不知道,需要有一种机制来在改变时触发某个操作
q: 为什么有了v-on,还要使用watch?
a: 因为v-on主要是响应一些标签动作,例如按钮的点击、拖拽、鼠标放上去等等,无法监听更多自定义的事件
例如,绑定了x、y两个响应式对象的输入框,修改任意一个输入框内容后,都会修改到响应式对象x和y的内容,通过watch函数可以监听到这两个变量的变化,当发生变化时及时修改计算结果
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="vue.global.js"></script>
</head>
<body>
<div id="app">
<input v-model="x">
<input v-model="y">
<div>{{xPlusY}}</div>
</div>
</body>
<script>
const {createApp, ref, watch} = Vue;
const app = {
setup() {
const x = ref("0");
const y = ref("0");
const xPlusY = ref(0);
watch(x,(newVal, oldVal)=>{
console.log(oldVal," -> ",newVal);
xPlusY.value = parseInt(x.value) + parseInt(y.value)
});
watch(y,(newVal, oldVal)=>{
console.log(oldVal," -> ",newVal);
xPlusY.value = parseInt(x.value) + parseInt(y.value)
});
return {
x,
y,
xPlusY
}
}
};
createApp(app).mount("#app")
</script>
</html>
computed计算函数
computed类似于watch函数,当computed中的数据发生修改后,会实时计算得到结果
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="vue.global.js"></script>
</head>
<body>
<div id="app">
<input type="text" name="aaa" id="aaa" v-model="a">
<input type="text" name="bbb" id="bbb" v-model="b">
<div>{{aPlusb}}</div>
</div>
</body>
<script>
const {createApp,ref,computed} = Vue;
const app = {
setup(){
let a = ref(1);
let b = ref(2);
let aPlusb = computed(()=>a.value + b.value);
return {a,b,aPlusb}
}
}
createApp(app).mount("#app")
</script>
</html>
-
computed计算的结果是只读的,即使在js中修改了结果,也会显示原来的内容,并提示不computed为readonly
let aPlusb = computed(()=>a.value + b.value); aPlusb = 123 //此时会显示告警:Write operation failed: computed value is readonly -
computed虽然计算结果是只读的,但是可以通过set方式进行赋值
let aPlusb = computed({ get: () => a.value + b.value, set: (val) => {a.value = val} }); // 当aPlusb发生修改时,会触发set,例如修改aPlusb为123,此时val为123,也会将a的值修改为123 aPlusb = 123
组件
项目工程中使用组件
项目工程中,一般由components、App.vue、main.js、index.html组合形成。其中:
-
components为各类自定义组件及成品组件
-
App.vue在
<script>标签中引入其他组件,完成拼接和setup,这里的App其实就相当于单文件中的app对象const app = { const aaa = "aaa"; setup(){ return {aaa}; } } -
main.js中引入App并完成挂载
createApp(App).mount('#app') -
index中定义app挂载的结点
<div id="app"></div>
但实际上,在main.js中执行mount操作前还可以进行其他配置,例如使用use方法进行组件配置:
const vm = createApp(App)
vm.use(xxx)
vm.use(xxx,{xxxx})
...
vm.mount("#app")
//vue.use是一个官方API,是全局注册一个组件或者插件的方法
单文件中使用组件
单文件挂载组件,类似于使用use注册组件的方式,先createApp,在使用component(component_name,{template:"<xxx></xxx>"})方法配置组件,最后再使用mount("#app")挂载app
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="vue.global.js"></script>
</head>
<body>
<div id="app">
<test-c/>
</div>
</body>
<script>
const {createApp} = Vue;
const app = {
setUp(){}
};
vm = createApp(app);
vm.component('test-c',{
template: "<div style='display: flex;flex-direction: column;align-items: center'><img src='logo1.png' height='126' width='134'/><span><br>this is a basic component test<span><div>"
});
vm.mount("#app")
</script>
</html>
前端路由
基本配置
-
router.js中配置路由信息:import { createRouter, createWebHistory } from 'vue-router' import a from "@/views/a.vue" import b from "@/views/b.vue" const router = createRouter({ history: createWebHistory(import.meta.env.BASE_URL), routes: [ { path: "/path_a", name: "path_a", component:a }, { path: "/path_b", name: "path_b", component:b }, ] }) export default router其中
path标识前端路由的路径地址信息name标识该路由的名称,这个名称是可以自定义的component当前路由所要渲染的组件 -
配置
<RouterView />注意:一般路由切换仅发生在RouterView 标签区域内,所以要对页面区域提前进行规划。
-
通过
router.push('/login')切换路由
存在多个RouterView
标准做法
-
对每个
RouterView都配置一个单独的name属性值,例如<home> <RouterView name="rv-a"/> <RouterView name="rv-b"/> </home> -
在route表中,配置不同路由下组件分别渲染到哪个
RouterView中import { createRouter, createWebHistory } from 'vue-router' import a from "@/views/a.vue" import b from "@/views/b.vue" const router = createRouter({ history: createWebHistory(import.meta.env.BASE_URL), routes: [ { path: "/path_a", name: "path_a", component:{ rv-a: a } }, { path: "/path_b", name: "path_b", component:{ rv-b: b } }, ] }) export default router例如上面的route表中,配置了a组件可以渲染在rv-a中,b组件可以渲染在rv-b中。
此时使用
router.push("/path_a")时,仅名称为"rv-a"的这个RouterView会渲染组件a同样的,使用
router.push("/path_b")时,仅名称为"rv-b"的这个RouterView会渲染组件b
异常情况
当未配置RouterView标签的名称时,或在route表中没有配置各个路由中组件所要渲染的RouterView标签时,通过router.push()方法切换路由,会在所有的RouterView中进行切换,会产生无法预计的效果。
路由匹配问题
会根据路径地址,在路由表router.js中寻找对应的路由配置,当存在路由时,切换视图。
存在相同的路径
-
当路由表中存在多个相同地址的路由,例如:
{ path: "/", name: "test", component:a }, { path: "/", name: "dev", component:b } }此时会根据匹配到的前后顺序,执行第一个,即名称为
test的路 -
当包含路径参数,例如:
{ path: "/", name: "test", component:a }, { path: "/:id", name: "dev", component:b } }此时会依据最长匹配原则进行匹配,如果访问的 URL 是
http://example.com/foo,则第二个路由/会被匹配到,并且参数id会被设置为"foo"。如果访问的 URL 是http://example.com/,则第一个路由/会被匹配到,而第二个路由/不会匹配。
存在相同的name
-
当存在相同的路由名称时,可能会导致路由冲突,例如:
{ path: "/test1", name: "test", component:a }, { path: "/test2", name: "test", component:b } }此时,访问
/test1失败,而访问/test2成功路由匹配的原则,是从上到下扫描一遍,寻找符合要求的路由,即使已经找到了,还会继续往下寻找(尝试寻找更为匹配的路由)
因此,单存在相同名称的路由时,访问
/test1,匹配到name名称属性为test的路由,但是继续往下匹配时,会找到另外一个名称为test的组件,此时访问/test1路径找到了两个路由,会无法匹配成功,提示[Vue Router warn]: No match found for location with path "/test1"。但是如果访问的是
test2,只有到第二个路由时,才会匹配成功,此时继续往下是没有名称为test的路由的,因此会匹配成功。
路由中的参数
路径参数
在路由表router.js中需要配置:
routes = [ { path: '/:参数名称1/:参数名称2/....', name: '路由名称', component: xxx } ]
使用方式1: 通过router.push("路径/参数值1/参数值2/.......")的方式传递
使用方式2: 通过router.push({path="路径",params={参数名称1:参数值1,参数名称2:参数值2}})的方式传递
使用方式3: 类似与方式2,通过路由名称而不是路径,即router.push({name="路由名称",params={参数名称1:参数值1,参数名称2:参数值2}})
查询参数
在路由表router.js中需要配置:
routes: [ { path: '/', name: '路由名称', component: xxx, props: (route) => ({ 参数名称1: route.query.参数名称1, 参数名称2: route.query.参数名称2 }) } ]
使用方式1: 通过router.push({path="路径",query={参数名称1:参数值1,参数名称2:参数值2}})的方式传递
使用方式2: 类似与方式1,通过路由名称而不是路径,通过router.push({name="路由名称",query={参数名称1:参数值1,参数名称2:参数值2}})的方式传递
状态参数
在使用状态参数传递参数信息时,不需要在
router.js文件中特殊处理路由定义和配置。状态参数不需要在路由表中进行定义和配置,而是通过router.push()或router.replace()方法来传递给目标路由,从而在目标组件中可以通过route.state访问状态参数。因此,在路由表中,只需要定义和配置路由的
path、name、component等基本属性,以及可选的props、meta状态参数不会出现在 URL 中,也不会保存在浏览器的历史记录中,而是只存在于当前路由的状态中。
使用方式1: 通过router.push({path="路径",status={参数名称1:参数值1,参数名称2:参数值2}})的方式传递
使用方式2: 类似与方式1,通过路由名称而不是路径,通过router.push({name="路由名称",status={参数名称1:参数值1,参数名称2:参数值2}})的方式传递
其他方式传递参数
除了使用路由参数外,还可以通过全局状态管理、浏览器缓存等方式存储参数内容
获取参数
可以使用this.$route的一系列方法获取路由参数,但是在vue3中没有this,需要使用useRoute来获取$router:
import { useRoute } from "vue-router";
const route = useRoute()
- 获取路径参数:
const pathParams = route.params - 获取查询参数:
const queryParams = route.query - 获取状态参数:
const statusParams = route.state
路径和视图切换
一般网页中,使用router.push("/")或<router-link to="/">会切换页面视图,但同时也会修改当前页面地址到对应的路由地址。
而在单页面应用程序中,一般情况下都是页面部分区域的切换,此时并不希望看到浏览器中路径地址发生改变,有以下两类方法:
- 使用
router.replace()方法而不是router.push()方法来进行页面切换 - 使用事件监听+渲染的方式进行,即根据事件行为,动态的修改页面上组件的渲染。
动态组件
对于页面组件切换,可以通过使用路由的方式来改变,或是使用i-if来动态渲染。不过一般更推荐使用<component :is="组件名称">动态组件的方式进行修改。
全局状态管理
- 类似于使用windows.变量名的方式,在vue中也可是将变量挂载在全局下,一般成为“全局状态”。
- 与windows挂载全局变量不同的是,vue中常说的全局状态管理,主要是指的具有“响应式对象”特性的全局变量。
- 全局状态管理依托于插件,例如vuex、piana、redux、mobx、rxjs、unstated等,一般vuex、piana使用较多
vuex
vuex通过在@/store/index.js中配置{state:{},mutation:{}}等信息,vuex的state中,变量数据是不可直接通过赋值的方式修改的,需要通过store.commit进行修改。
piana
与vuex不同的是,piana中每个全局状态都是一个单独的js文件,例如@/store/var1.js、@/store/var2.js等,
并通过在其中配置变量、方法来实现全局状态管理。例如:
import { ref, computed } from 'vue'
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', () => {
const count = ref(1)
const doubleCount = computed(() => count.value * 2)
function increment() {
count.value++;
}
return { count, doubleCount, increment }
})
特殊
1. <script setup>语法糖
-
参考文档:cn.vuejs.org/api/sfc-scr…
老版本的vue3中,在script标签中引入组件,需要使用
export default语法:<script> import 引入的组件名称1 from 'path'; import 引入的组件名称2 from 'path'; export default { name:'当前组件的名称', props:{ 当前组件的属性1:String, 当前组件的属性2:Number } components:{ 引入的组件名称1, 引入的组件名称2 } } </script>新版本中,可以直接使用
<script setup>完成这一步骤:<script setup> import 引入的组件名称1 from 'path'; import 引入的组件名称2 from 'path'; defineProps({ 当前组件的属性1: { type: String, required: true }, 当前组件的属性2: { type: Number, required: true } }) </script>
2. element-ui
element-ui目前默认是使用的vue2版本的,在vue3中直接使用npm i element-ui -S命令会提示依赖不支持
因此,如果要在vue3中使用element-ui,需要使用到element-plus版本:element-plus.gitee.io/zh-CN/guide…
(1) 使用npm install element-plus --save安装element-plus
(2) 配置自动导入:
// 1. 先安装两个插件: npm install -D unplugin-vue-components unplugin-auto-import
// 2. 然后在vite.config.ts中修改配置
import { defineConfig } from 'vite'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
export default defineConfig({
// ...
plugins: [
// ...
AutoImport({
resolvers: [ElementPlusResolver()],
}),
Components({
resolvers: [ElementPlusResolver()],
}),
],
})
(3) 此时,就不需要在使用组件的时候,通过import 组件名 from path的方式导入组件,可以直接使用element-ui的组件:
// App.vue
// 可以看到,script中没有导入“el-input-number”组件,却可以直接当做标签来使用了
<script setup>
import { ref } from 'vue'
const num = ref(1)
function handleChange(value) {
console.log(value);
}
</script>
<template>
<header></header>
<main>
<el-input-number v-model="num" @change="handleChange" :min="1" :max="10"
label="描述文字">
</el-input-number>
</main>
</template>
<style scoped></style>
3. 在其他js文件中使用pinia
在vue模块中使用pinia时,由于pinia已经在main.js中注册了,因此是可以正常使用的
main.js:
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
const app = createApp(App)
app.use(createPinia())
app.mount('#app')
而在其他的js文件中使用pinia时,由于js没有被vue托管,是无法直接使用pinia的,会报错:getActivePinia was called with no active Pinia. Did you forget to install pinia?
因此需要重新在这个js文件中注册一下pinia。
步骤1:为pinia单独新建一个文件store.js
import { createPinia } from 'pinia';
const pinia = createPinia();
export default pinia;
步骤2:然后在要使用pinia的js文件中导入
import pinia from '@/stores/store'
import { useSystemDataStore } from "@/stores/index";
const SystemDataStore = useSystemDataStore(pinia);
3. 箭头函数问题
()=>{}和()=>是不一样的,(a)=>{b}想当与function(a){b},而(a)=>b则是function(a){return b}。因此在vue的computed、watch等地方使用箭头函数时要注意进行区分。
4. 路由模式对地址及nginx配置的影响
前端路由存在history和hash两种模式
hash模式
当使用hash模式时,路由地址前需要包含#号,即url/#/api_path,在这种方式下,nginx不需要特殊配置即可。
history模式
当使用history模式时,路由地址不包含#号,但此时直接访问时会提示403错误。这是因为打包成静态资源后,访问的是xxx.html,而不是访问的vue前端路由组件。
解决方案是需要在nginx中配置将所以相关的路由请求都转发到index.html中,此时index.html会对请求进行处理,获取到对应的数据:
1
2
3 events {
4
5 }
6
7
8 http {
9 include mime.types;
10 server {
11 listen 80;
12 server_name localhost;
13
14 location / {
15 root /usr/local/etc/nginx/www;
16 index index.html index.htm;
17 try_files $uri $uri/ /index.html;
18 }
19 location /api {
20 proxy_pass http://localhost:12345;
21 }
22
23 }
24 }
关键就在于try_files $uri $uri/ /index.html;这个配置,它会将所有匹配不到的请求转发到index.html中。
5. 引用
js中引用组件,在vue2中可以使用this.$refs方式:
<p ref="p">hello</p>
// 通过this.$refs使用模板标签:
this.$refs.p.style = "red"
但是在vue3中,没有this上下文,需要使用其他方式进行:
步骤1:配置一个ref来接受组件引用:
const p = ref()
步骤2:与vue2类似的,通过ref对标签命名
<p ref="p">hello</p>
步骤3:通过p.value使用引用:
p.value.style = "red"
应用举例:
const elrow = ref() watch(elrow,()=>{ const e = elrow.value.$el e.getElementsByTagName("span") spanTags.forEach(spanTag=> { spanTag.style.color = "red" }); } <el-row class="rowClass" ref="elrow"> <span>test</span> </el-row>需要注意的是,对于html原生标签例如div,在使用时直接通过
elrow.value就可以获取到数据了,不需要在使用其$el属性:const elrow = ref() watch(elrow,()=>{ const e = elrow.value // 注意这里没有使用其$el属性 e.getElementsByTagName("span") spanTags.forEach(spanTag=> { spanTag.style.color = "red" }); } <div class="rowClass" ref="elrow"> <span>test</span> </div>
6. $
进行命名。
常用的$函数包括:
- $attrs: 包含父作用域中不被当前组件识别的特性绑定(attribute bindings)和事件(event listeners)。
- $createElement: 创建 VNode 的方法。
- $data: 组件实例观察的数据对象。
- $el: 组件实例的根 DOM 元素。
- $emit: 用于触发事件的方法。
- $options: 组件的初始化选项。
- $parent: 父实例。
- $props: 当前组件接收到的 props 对象。
- $root: 应用的根组件实例。
- $scopedSlots: 作用域插槽内容对象。
- $slots: 插槽内容对象。
- $store: 当前组件树的 Vuex store 实例。
- $watch: 观察实例上的属性或计算属性的方法。
7. 生命周期问题
在vue中,js代码及template模板写在同一个文件中,渲染是有时间前后顺序的。
声明周期函数
一般来说,当存在生命周期函数时,按生命周期函数的加载顺序进行: (1). 创建阶段:执行beforeCreate和created钩子函数 (2). 挂载阶段:执行beforeMount,计算模板的DOM,然后执行mounted (3). 更新阶段:执行beforeUpdate和updated (4). 销毁阶段:执行beforeDestroy和destroyed
即:
JavaScript代码(beforeCreate/created) → 模板渲染 → JavaScript代码(mounted/beforeUpdate/updated)
普通js代码
而对于非生命周期函数的js代码,会在组件渲染前执行,并且优先于整个生命周期函数,即:
普通 JavaScript 代码 → beforeCreate → created → beforeMount → 模板渲染 → mounted → beforeUpdate → updated → beforeDestroy → destroyed
undefined问题
因此在执行时,直接使用ref获取模板元素,可能会得到undefined:
const a = ref()
console.log(a.value) // undefined
<tag-a ref="a"/>
但是可以看到a是一个响应式对象,因此是可以使用watch、computed方式进行跟踪处理的:
const a = ref()
watch(a,()=>{console.log(a.value.$el)})
<tag-a ref="a"/>
8. axios响应头字段缺失
打印axios的响应头response.headers,会发先只有非常少的字段:
而查看浏览器抓包数据会发现headers是正常的
这是因为如果请求跨域了,浏览器会进行拦截处理,此时服务端的跨域头文件配置会生效,只有在这里指定的字段能够在axios.response.headers中显示出来
例如当需要在相应头中获取refreshToken,则需要配置response.setHeader("Access-Control-Expose-Headers", "refreshToken");。
stackoverflow参考问题:
9.子组件向父组件传递参数
子组件抛出事件,并在事件中附带参数,父组件在使用时,可直接使用该参数:
//子组件 sub-component:
const emits = defineEmits(["test"])
emits("test",123)
// 父组件
// 注意这里是直接绑定到doSomething
const doSomething = (value)=>{console.log(value)} //打印123
<templete>
<sub-component @test="doSomething">
</sub-component>
</templete>
而如果在@test=“doSomething”中直接调用函数,并尝试将子组件传递过来的参数传给函数形参,例如@test=“doSomething(val)”,会提示undefined:
//子组件 sub-component:
const emits = defineEmits(["test"])
emits("test",123)
// 父组件
// 注意这里是doSomething(val)
const doSomething = (value)=>{console.log(value)} //打印undefined
<templete>
<sub-component @test="doSomething(val)">
</sub-component>
</templete>
这里可以通过使用$event来接受这个参数,并且也只能使用$event来接受这个子组件传过来的参数:
//子组件 sub-component:
const emits = defineEmits(["test"])
emits("test",123)
// 父组件
// 注意这里是doSomething($event)
const doSomething = (value)=>{console.log(value)}//打印123
<templete>
<sub-component @test="doSomething($event)">
</sub-component>
</templete>
但是如果父组件调用函数时还传递了其他参数,例如列表循环渲染时当前的index,此时要在函数中同时处理子组件传过来的参数和当前索引参数,则类似于上面的方式,可以使用$event来接收
//子组件 sub-component:
const emits = defineEmits(["test"])
emits("test",123)
// 父组件
// 注意这里是doSomething($event)
const list0 = [1,2,3]
const doSomething = (sub_parm,value)=>{console.log(sub_parm,value)}//打印1 123;2 123;3 123
<templete>
<div v-for="(item,index) in list0" @test="doSomething($event,index)">
</div>
</templete>
并且这个$event可以放在前面也可以放在后面:
//子组件 sub-component:
const emits = defineEmits(["test"])
emits("test",123)
// 父组件
// 注意这里是doSomething($event)
const list0 = [1,2,3]
const doSomething = (value,sub_parm)=>{console.log(sub_parm,value)}//打印1 123;2 123;3 123
<templete>
<div v-for="(item,index) in list0" @test="doSomething(index,$event)">
</div>
</templete>
9.toRaw
vue中有很多proxy代理的数据,有些可以通过value获取,有些无法通过value获取,但是都可以通过使用toRaw(xxx)的方式来获取原始内容。
10.Proxy代理和响应式的区别
vue中,响应式对象一般都是通过proxy代理的方式进行的,所以一般都是proxy实例对象。但并不意味着proxy都是响应式对象。
例如props
defineProps 使用 Proxy 代理了 props,目的是实现只读效果和警告,但并不会使 props 成为响应式的,原因是:- props 代表外部传入的值,组件不应直接修改 props,这可能导致外部状态的变更; - 组件内部可以在 computed 或 watcher 响应 props 的变化,重新计算内部的值并触发重新渲染,但不会直接修改 props。
11. reactive响应式对象的特殊性
reactive对象可以通过.value来获取真实数据内容,但是却无法通过.value=xxx的方式来对其进行赋值操作。这时因为其与ref响应式对象的机制是不同的。
操作reactive响应式对象时,需要将其当做普通数据来看待
例如const a = reactive([]),操作a这个响应式对象,需要按照操作数组的方式进行,比如说a.push(xxx)、a.splice(xxx),而如果想要让其等于一个新的数组,需要进行数组的重新赋值操作:a.splice(0, a.length, ...newArray)
同样的,如果是对象的数据const b = reactive({"aaa":123}),也是要当做普通对象的操作方式来处理的:b["aaa"]=321
12. 监听props问题
props是非响应式对象,因此如果想要监听props,是需要传入一个响应式对象的,这就要求
(1) 父组件中使用:或-vbind方式传入的应当是一个被ref、reactive、computed包装或计算过的响应式对象
(2) 子组件的defineProps中属性类型定义为Object
13. vue-dragable拖拽失败问题
在vue中,如果拖拽组件的数据是一个reactive的,拖拽会失效,而如果是一个ref的,则能正常使用。这很反常规,因为reactive是深层响应能检测到对象内容改变,而ref只能检测到对象引用改变。
这实际上也是因为reactive和ref的机制差异导致的,reactive需要通过set等方式修改数据,而ref则使用value的方式修改数据。
而另外一个问题是,ref只能检测到引用发生改变,为什么使用拖拽组件仅仅是改变顺序,也能进行响应。
根据以下源码分析,我们可以得知,在使用v-model传入数据到dragable组件时,数据将传入到value而非list属性中。这时在拖拽发生数据改变时,沿着其函数调用栈,会发现最终其实是创建了一个新的listconst newList = [...this.value],并通过onList(newList);在这个新的list上修改了数据的顺序,并最终通过this.$emit("input", newList);返回结果给父组件。因此父组件接收到的其实是一个新的list数据,因此在使用ref时,相当于是修改了数据的引用,因此能够顺利触发响应式变化。
onDragUpdate(evt) {
removeNode(evt.item);
insertNodeAt(evt.from, evt.item, evt.oldIndex);
const oldIndex = this.context.index; // 获取原来的位置索引
const newIndex = this.getVmIndex(evt.newIndex); // 获取新的位置索引
this.updatePosition(oldIndex, newIndex); // 触发列表更新操作
const moved = { element: this.context.element, oldIndex, newIndex };
this.emitChanges({ moved });
},
updatePosition(oldIndex, newIndex) {
// 这个updatePosition是一个箭头函数
// 这个函数会根据原数组、元素原始位置、新位置进行调整,并返回该调整后的数组
const updatePosition = list =>
list.splice(newIndex, 0, list.splice(oldIndex, 1)[0]);
// 这里使用了list.splice(newIndex, 0, newElement)的方式插入数据,第二个参数为0表示插入模式
// 首先list.splice(oldIndex, 1)会删除旧位置元素内容
// 返回该数据会,通过list.splice(oldIndex, 1)[0]可以获取到该元素
// list.splice(newIndex, 0, list.splice(oldIndex, 1)[0]);返回了这个数组的更新顺序后的呢绒
this.alterList(updatePosition); //这里是直接传了一个函数到alterList函数中
},
alterList(onList) {
// 这里的onList就是updatePosition(list),注意list是一个函数参数
// 这个函数中会对dragable的数据传入方式进行判断
// 如果传入dragable的是list,那么会直接在这个list上修改并返回
if (this.list) {
onList(this.list);
return;
}
// 而如果传入的是value,则会先展开value,创建一个list,修改后并返回
// 根据官方文档中对value和list参数的描述,在使用v-model时是value而非list
const newList = [...this.value];
onList(newList);
this.$emit("input", newList);
},
list: {
type: Array,
required: false,
default: null
},
value: {
type: Array,
required: false,
default: null
},
/*
使用v-model时,是用的value而非list:
value
Type: Array
Required: false
Default: null
Input array to draggable component. Typically same array as referenced by inner element v-for directive.
This is the preferred way to use Vue.draggable as it is compatible with Vuex.
It should not be used directly but only though the v-model directive:
<draggable v-model="myArray">
list
Type: Array
Required: false
Default: null
Alternative to the value prop, list is an array to be synchronized with drag-and-drop.
The main difference is that list prop is updated by draggable component using splice method, whereas value is immutable.
Do not use in conjunction with value prop.
*/