vue笔记【更新中】

178 阅读3分钟

创建vue应用

传统方式创建vue应用

  1. 下载vue.global.js单文件

    unpkg.com/vue@3.0.0-b…

  2. script标签中引入js文件

  3. script标签中挂载vue

    1. 从vue.global.js的Vue中引入“CreateApp”:const {createApp} = Vue;

    2. 配置一系列需要由vue托管的变量及函数:const a = 123; function b(){}

    3. 创建由Vue托管的app对象,将上面说到的变量和函数通过setup函数进行返回

      const app = {
      setup(){
      return {a,b}
      }
      }
      
    4. 使用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

  1. 创建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/
  2. 进入app路径 cd <your-project-name>

  3. 更新依赖 npm install

  4. 运行程序npm run devnpm run build

方法2: vue@cli

  1. 全局安装cli: sudo npm install -g @vue/cli
  2. 创建项目: vue create 项目名称
  3. 之前较早时期,使用vue@cli创建的默认是Vue2的应用程序,还需要使用命令进行更新:vue add vue-next

方法3: vite

  1. 创建vue应用npm init vite-app 项目名称
  2. 更新依赖 npm install
  3. 运行程序npm run devnpm 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 : 描述依赖包和版本情况,以及自定义脚本

  1. 在src/components/xxx.vue中配置setup函数,配置<template>标签数据,其整体会作为一个组件返回

    <script setup>
    // 配置组件参数(属性)
    defineProps({
     msg: {
       typeString,
       requiredtrue
    }
    })
    </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-weight500;
     font-size2.6rem;
     top: -10px;
    }
    ​
    h3 {
     font-size1.2rem;
    }
    ​
    .greetings h1,
    .greetings h3 {
     text-align: center;
    }
    ​
    @media (min-width1024px) {
    .greetings h1,
    .greetings h3 {
       text-align: left;
    }
    }
    </style>
  2. 在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-height1.5;
    }
    ​
    .logo {
     display: block;
     margin0 auto 2rem;
    }
    ​
    @media (min-width1024px) {
     header {
       display: flex;
       place-items: center;
       padding-rightcalc(var(--section-gap) / 2);
    }
    ​
    .logo {
       margin0 2rem 0 0;
    }
    ​
     header .wrapper {
       display: flex;
       place-items: flex-start;
       flex-wrap: wrap;
    }
    }
    </style>
  3. 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>
  4. 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标签

image.png

<!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-ifv-else-ifv-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>
  1. 使用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>
    
  2. 将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,一般常见于selecttypeareainput等输入交互标签

  • 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)

image.png

reactive

const b = reactive({"val":123})
console.log(b)
b = {"val":123}
console.log(b)

image.png

watch监听函数

双向数据绑定可以实现数据的双向流动,数据发生修改时,可以通过watch来修改触发监听函数

q: 为什么有了响应式对象,能获取到对象的最新值,还要使用watch?

a: 这是因为对象数据改变是实时的,但是这个时间我们却不知道,需要有一种机制来在改变时触发某个操作

q: 为什么有了v-on,还要使用watch?

a: 因为v-on主要是响应一些标签动作,例如按钮的点击、拖拽、鼠标放上去等等,无法监听更多自定义的事件

例如,绑定了xy两个响应式对象的输入框,修改任意一个输入框内容后,都会修改到响应式对象xy的内容,通过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>

前端路由

基本配置

  1. 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当前路由所要渲染的组件

  2. 配置<RouterView />

    注意:一般路由切换仅发生在RouterView 标签区域内,所以要对页面区域提前进行规划。

  3. 通过router.push('/login')切换路由

存在多个RouterView

标准做法

  1. 对每个RouterView都配置一个单独的name属性值,例如

    <home>
      <RouterView name="rv-a"/>
      <RouterView name="rv-b"/>
    </home>
    
  2. 在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中寻找对应的路由配置,当存在路由时,切换视图。

存在相同的路径

  1. 当路由表中存在多个相同地址的路由,例如:

     {
          path: "/",
          name: "test",
          component:a
        },
        {
          path: "/",
          name: "dev",
          component:b
        }
    }
    

    此时会根据匹配到的前后顺序,执行第一个,即名称为test的路

  2. 当包含路径参数,例如:

     {
          path: "/",
          name: "test",
          component:a
        },
        {
          path: "/:id",
          name: "dev",
          component:b
        }
    }
    

    此时会依据最长匹配原则进行匹配,如果访问的 URL 是 http://example.com/foo,则第二个路由 / 会被匹配到,并且参数 id 会被设置为 "foo"。如果访问的 URL 是 http://example.com/,则第一个路由 / 会被匹配到,而第二个路由 / 不会匹配。

存在相同的name

  1. 当存在相同的路由名称时,可能会导致路由冲突,例如:

     {
          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 访问状态参数。

因此,在路由表中,只需要定义和配置路由的 pathnamecomponent 等基本属性,以及可选的 propsmeta

状态参数不会出现在 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()
  1. 获取路径参数:const pathParams = route.params
  2. 获取查询参数:const queryParams = route.query
  3. 获取状态参数:const statusParams = route.state

路径和视图切换

一般网页中,使用router.push("/")<router-link to="/">会切换页面视图,但同时也会修改当前页面地址到对应的路由地址

而在单页面应用程序中,一般情况下都是页面部分区域的切换,此时并不希望看到浏览器中路径地址发生改变,有以下两类方法:

  1. 使用router.replace()方法而不是router.push()方法来进行页面切换
  2. 使用事件监听+渲染的方式进行,即根据事件行为,动态的修改页面上组件的渲染。

动态组件

对于页面组件切换,可以通过使用路由的方式来改变,或是使用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命令会提示依赖不支持截屏2023-03-05 18.27.24

因此,如果要在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. $

函数是vue组件实例的一些内置函数,为了与用户自定义函数区分,使用函数是vue组件实例的一些内置函数,为了与用户自定义函数区分,使用进行命名。

常用的$函数包括:

  • $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,会发先只有非常少的字段:

image.png

而查看浏览器抓包数据会发现headers是正常的

image.png

这是因为如果请求跨域了,浏览器会进行拦截处理,此时服务端的跨域头文件配置会生效,只有在这里指定的字段能够在axios.response.headers中显示出来

例如当需要在相应头中获取refreshToken,则需要配置response.setHeader("Access-Control-Expose-Headers", "refreshToken");

stackoverflow参考问题:

stackoverflow.com/questions/6…

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方式传入的应当是一个被refreactivecomputed包装或计算过的响应式对象

(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.
*/