Skip to content

Vue3

简介

概述

Vue是一种流行的JavaScript框架,用于构建用户界面和单页面应用程序(SPA)。

Vue (发音为 /vjuː/,类似 view) 是一款用于构建用户界面的 JavaScript 框架。它基于标准 HTML、CSS 和 JavaScript 构建,并提供了一套声明式的、组件化的编程模型,帮助你高效地开发用户界面。无论是简单还是复杂的界面,Vue 都可以胜任。

Vue基于MVVM(模型-视图-视图模型)架构,它允许开发人员构建可重用的组件,并且可以很容易地将组件组合起来形成复杂的UI。Vue非常灵活,支持多种方式来定义组件的样式、逻辑和行为。Vue的另一个优点是其响应式系统,它可以自动追踪数据的变化,并在必要时更新UI。

Vue还提供了一系列工具和插件,包括Vue CLI(用于构建Vue项目的命令行界面工具)、Vue Router(用于构建SPA的路由库)和Vuex(用于管理Vue应用程序的状态管理库)等。

MVVM 双向数据绑定模式

  • Model:模型层,在这里表示 JavaScript 对象

  • View:视图层,在这里表示 DOM(HTML 操作的元素)

  • ViewModel:连接视图和数据的中间件,Vue.js 就是 MVVM 中的 ViewModel 层的实现者

在 MVVM 架构中,是不允许数据视图直接通信的,只能通过 ViewModel 来通信,而 ViewModel 就是定义了一个Observer` 观察者

  • ViewModel 能够观察到数据的变化,并对视图下对应的内容进行更新

  • ViewModel 能够监听到视图的变化,并能够通知数据发生改变

Vue.js 就是一个 MVVM 的实现者,他的核心就是实现了 DOM 监听 与 数据绑定

Vue.js 的两大核心要素

数据驱动

当你把一个普通的 JavaScript 对象传给 Vue 实例的 data 选项,Vue 将遍历此对象所有的属性,并使用 Object.defineProperty 把这些属性全部转为 getter/setter。Object.defineProperty 是 ES5 中一个无法 shim 的特性,这也就是为什么 Vue 不支持 IE8 以及更低版本浏览器。

这些 getter/setter 对用户来说是不可见的,但是在内部它们让 Vue 追踪依赖,在属性被访问和修改时通知变化。这里需要注意的问题是浏览器控制台在打印数据对象时 getter/setter 的格式化并不同,所以你可能需要安装 vue-devtools 来获取更加友好的检查接口。

每个组件实例都有相应的 watcher 实例对象,它会在组件渲染的过程中把属性记录为依赖,之后当依赖项的 setter 被调用时,会通知 watcher 重新计算,从而致使它关联的组件得以更新。

组件化

  • 页面上每个独立的可交互的区域视为一个组件

  • 每个组件对应一个工程目录,组件所需的各种资源在这个目录下就近维护

  • 页面不过是组件的容器,组件可以嵌套自由组合(复用)形成完整的页面

快速入门

用插件表达式完成数据展示到前端 用v-model完成视图操作数据

html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Title</title>
    <script src="https://cdn.bootcss.com/vue/3.2.47/vue.global.min.js"></script>
</head>
<body>

<div id="app">
    <span>{{ msg }}</span><br/>
    <input type="text" v-model="msg">
</div>

</body>
<script>
    Vue.createApp({
        data() {
            return {
                msg: "hello vue!"
            }
        }
    }).mount("#app);
</script>
</html>

Vue内部指令

条件指令

  • v-if

  • v-else-if

  • v-else

  • v-show

条件指令中的if-elseif-else与show的区别在于 如果条件不满足if-elseif-else的元素根本在DOM就没有,但是show还是有的,只不过不显示而已 在用户层面,没有任何区别上的感知 所以具体在项目中到底用哪一个,哪一个都行

html
<div id="app">
    <input v-model="score"><br/>

    <span v-if="score >= 90">优</span>
    <span v-else-if="score >= 80">良</span>
    <span v-else-if="score >= 60">及格</span>
    <span v-else>不及格</span>
</div>
html
<div id="app">
    <button v-show="login">退出</button>
    <button v-show="!login">登录</button>
</div>

循环指令

  • v-for
html
<div id="app">
    <ul>
        <li v-for="menuitem of menu">{{menuitem}}</li>
    </ul>
</div>

内容设置指令

  • v-html

  • v-text

  • {{}}

html
<div id="app">
    <span v-text="inner"></span>
</div>
html
<div id="app">
    <span v-html="inner"></span>
</div>

数据绑定指令

  • v-model

  • v-bind

  • :

凡是可以与用户交互的元素,都可使用v-model

当需要设置元素属性时,属性值来源于vue当中的变量,就需要使用v-bind

html
<div id="app">
    <span>{{msg}}</span><br/>
    <input v-model="msg">
</div>
html
<div id="app">
    <img v-bind:src="img">
</div>
html
<div id="app">
    <img :src="img">
</div>

事件绑定指令

  • v-on

  • @

  • @click.stop, 阻止冒泡

  • @click.prevent, 阻止跳转

  • @click.self, 阻止子冒泡

  • @click.once, 点击后立即解绑

在Vue.createApp时,除了data以外,需要指定methods,每个方法如下填写:

js
Vue.createApp({
    methods: {
        fn1: function () {
            console.log("fn1");
        },
        fn2: function () {
            console.log("fn2");
        }
    }
}).mount("#app");

在html标签上,编写v-on指令

html
<div id="app">
    <button v-on:click="fn1">按钮1</button>
    <button @click="fn2()">按钮2</button>
</div>

Idea中报红的处理

  1. 打开 Idea 全局配置
  2. 找到菜单项: Editor -> Inspections -> XML
  3. 取消勾选Unbound namespace prefix,确认

全局API

data

绑定数据

methods

绑定方法

计算属性

  • computed

计算: 行为 属性: 状态 目的在于,想要初始化某个属性时,需要经历一些过程 并不是每一次调用,都会触发 只会在第一次调用时才会被触发

html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <script src="https://cdn.bootcss.com/vue/3.2.47/vue.global.min.js"></script>
</head>

<body>

<div id="app">
    件数: <input type="text" v-model="count"><br/>
    单价: <input type="text" v-model="price"><br/>
    总价: <input type="text" v-model="total" readonly disabled>
</div>

</body>

<script>
    Vue.createApp({
        data: {
            count: 1,
            price: 9.9
        },
        computed: {
            total() {
                return this.count * this.price;
            }
        }
    }).mount("#app");
</script>

</html>

监听器

  • watch

用于监听某属性的变化 既得到旧值,又能得到新值

javascript
Vue.createApp({
    data() {
        return {
            count:1,
            minusDisable: true
        }
    },
    methods: {
        minus() {
            this.count--;
        },
        plus() {
            this.count++;
        }
    },
    watch: {
        count(newval, oldval) {
            if (newval == 1) {
                this.minusDisable = true;
            } else {
                this.minusDisable = false;
            }
        }
    }
}).mount("#app");

过滤器【了解】

过滤器是对即将显示的数据做进一步的筛选处理,然后进行显示,值得注意的是过滤器并没有改变原来的数据,只是在原数据的基础上产生新的数据。

过滤器分全局过滤器和本地过滤器(局部过滤器)。

需要特别注意的是:过滤器中不能使用this指针。

全局过滤器

下面定义一个全局过滤器,用于在数据前加上大写的VUE。需要注意的是,全局过滤器定义必须始终位于Vue实例之上,否则会报错。

html
<div id="app">
    {{message | toAdd}}
</div>

<script src="https://cdn.bootcss.com/vue/3.2.47/vue.global.min.js"></script>
<script>
    Vue.filter("toAdd", function(value) {
        return 'VUE' + value
    })
    Vue.createApp({
        data: {
            message: '过滤器',
        }
    }).mount("#app");
</script>

注意

  1. Vue.filter() 后有两个参数:过滤器名,过滤器处理函数。
  2. 过滤器处理函数也有一个参数:要过滤的数据。

本地过滤器

本地过滤器存储在vue组件中,作为filters属性中的函数,我们可以注册多个过滤器存储在其中。

html
<div id="app">
    <p>{{message | Reverse}}</p>
    <p>{{message | Length}}</p>
</div>

<script src="https://cdn.bootcss.com/vue/3.2.47/vue.global.min.js"></script>
<script>
    Vue.createApp({
        data: {
            message: '过滤器',
        },
        filters: {
            Reverse: function(value) {
                if (!value){
                    return '';
                } 
                return value.toString().split('').reverse().join('');
            },
            Length: function(value) {
                return value.length;
            },
        },
    }).mount("#app")
</script>

过滤器传参

过滤器也是可以传递参数的。

html
<div id="app">
    {{msg1 | toDou(1,2)}}
    {{msg2 | toDou(10,20)}}
</div>

<script src="https://cdn.bootcss.com/vue/3.2.47/vue.global.min.js"></script>
<script>
    Vue.filter('toDou', function(value, param1, param2) {
        if (param2 < 10) {
            return value + param1;
        } else {
            return value + param2;
        }
    });
    Vue.createApp({
        data: {
            msg1: 9,
            msg2: 12,
        }
    }).mount("#app")
</script>

注意

过滤器处理函数的第一个参数,仍然是要过滤的数据。从第二个参数开始,才是过滤器本身要传递的参数。

串联过滤器

多个过滤器可以串联使用

html
<div id="app">
    {{money | prefix | suffix}}
</div>

<script src="https://cdn.bootcss.com/vue/3.2.47/vue.global.min.js"></script>
<script>
    Vue.filter("prefix", function(value) {
        return '¥' + value
    })
    Vue.filter("suffix", function(value) {
        return  value + '元'
    })
    Vue.createApp({
        el: '#app',
        data: {
            money:10
        }
    }).mount("#app")
</script>

Vue钩子函数

也叫做全局函数 也叫做生命周期函数

  • VUE 生命周期所经历的四件事

    1. 创建 - 一次

    beforeCreate

    created

    1. 挂载 - 一次

    beforeMount

    mounted

    1. 更新 - N 次

    beforeUpdate

    updated

    1. 销毁 - 一次

    beforeDestroy(vue2)

    destroyed(vue2)

    beforeUnmount(vue3)

    unmounted(vue3)

Vue模板

  • template
html
<body>

<div id="app"></div>

</body>
javascript
Vue.createApp({
    el: "#app",
    template: `
        <div>
        <span>{{msg}}</span><br/>
        <input v-model="msg">
        </div>
    `,
    data() {
        return {
            msg: "Hello Vue!"
        }
    },
    methods: {}
}).mount("#app")

单文件组件

安装node与pnpm

  1. 去官网下载并安装nodejs

    https://nodejs.org

  2. 测试安装是否成功

    注意

    npm(node package manager,node包管理器)会随着node的安装,自动安装。但是npm的版本号和node的版本号不是一致的

    bash
    node -v
    npm -v
  3. 安装pnpm

    npm i -g pnpm

  4. 测试pnpm

    pnpm -v

创建vue脚手架项目

  1. 运行以下命令创建项目

    pnpm create vue

  2. 控制台交互

    交互窗口

    • Project name: 项目名称

    • Add TypeScript? No

    • Add JSX Support? No

    • Add Vue Router for Single Page Application development? Yes

    • Add Pinia for state management? Yes

    • Add Vitest for Unit Testing? No

    • Add an End-to-End Testing Solution? No

    • Add ESLint for code quality? No

    • Add Vue DevTools extension for debugging? (experimental)? Yes

  3. 安装依赖

    bash
    cd 项目名称
    pnpm install
    pnpm dev

脚手架项目结构

  • public

    用于存储静态资源

  • index.html

    全局html,vue项目是单页面项目,肉眼可见的页面转换,其实是单页面切换组件

  • src

    • main.js

      通过main.js创建根组件,挂载到index.html相应的位置 此处用脚手架创建的项目是挂载到了id为app的元素上

    • App.vue

      在main.js把App.vue的组件,加载到了index.html上

  • vite.config.js

    vue脚手架项目的全局配置

  • package.json

    里面维护了本项目需要依赖的模块、本项目能够使用的命令...

集成Idea

  1. 点击 Idea 窗口上边的Edit Configurations
  2. 点击弹出窗口左上角的加号
  3. 选择 npm
  4. 在 script 的下拉菜中选择dev
  5. 再点 OK
  6. 窗口上边的运行和 debug 按钮就可以点
  7. 在点之前,一定要确认左侧的到底是 build 还是 lint 还是 serve

单文件组件开发

template标签

以前怎么写,现在还怎么写

script标签

注意组件不再写template属性 注意要导出export default 注意要导入import CartView from './views/CartView' 注意引用子组件:

components: {
    CartView: CartView
}

style标签

分别能够使用lang属性和scoped属性

scoped属性用于描述样式只在当前组件中生效 但是注意也区别于父组件和子组件 如果在父组件里边,哪怕写了scoped属性,那么父组件里面的所有子组件依然能够继承样式

lang属性可以指定预编译css 在使用脚手架创建本项目时,指定预编译css使用scss/sass

<style lang='scss' scoped>
ul {
    background: antiquewhite;
    li {
        color: red; 
    }
}
</style>

组件化开发

Vue3语法

javascript
Vue.createApp({
    data() {},
    methods: {},
    computed: {},
    watch: {},
    filters: {},
    template: ``,
    props: [],
    emits: []
}).mount("#app")

组件的父子关系-铺垫

  • ChildView.vue
vue
<template>
  <div>{{msg}}</div>
  <span>{{msg}}</span>
</template>

<script>
export default {
  data() {
    msg: 'Hello Vue'
  }
}
</script>
  • App.vue
vue
<template>
  <div>
    <ChildView></ChildView>
  </div>
  <div>
    <ChildView></ChildView>
  </div>
</template>

<script>
import ChildView from "@/views/ChildView.vue";

export default {
  components: {
    ChildView
  }
}
</script>

组件通信

  • props down

  • events up

  • ref 通信

    • $refs
  • 事件总线

props通信【单向传递】

注意

所有的props都遵循着单向绑定原则,props 因父组件的更新而变化,自然地将新的状态向下流往子组件,而不会逆向传递。

props传向子组件中的数据,无法在子组件中修改。

参考官方文档:

https://cn.vuejs.org/guide/components/props.html#one-way-data-flow

父组件知道数据,子组件不知道,需要由父组件传给子组件

在子组件中,需要告知我子组件需要某个属性,由别人传过来

用props关键字告知

在父组件中,给子组件传值

假设给子组件传name

以下方式传的是字面值

<component name="传给你的值"></component>

以下方式传的是表达式的结果

<component :name="cartname"></component>

  • TodoItem.vue
vue
<template>
  <li>
    <span>{{info.task}}</span>
    <input type="checkbox" v-model="info.complete">
  </li>
</template>

<script>
export default {
  props: ["info"]
}
</script>
  • App.vue
vue
<template>
  <ul>
    <TodoItem
        v-for="item of todoList"
        :info="item"/>
  </ul>
</template>

<script>
import TodoItem from "@/views/TodoItem.vue";

export default {
  components: {TodoItem},
  data() {
    return {
      todoList: [{
        task: "完成数学课作业",
        complete: true
      }, {
        task: "准备化学实验报告",
        complete: false
      }, {
        task: "参加历史课的小组讨论",
        complete: false
      }, {
        task: "查找关于生物学项目的研究资料",
        complete: false
      }]
    }
  }
}
</script>

v-model通信【双向传递】

https://cn.vuejs.org/guide/components/v-model.html

  • 父组件
vue
<template>
  <DiseaseSearchDialog v-model="diseaseSearchDialogVisible"/>
</template>

<script>
  data() {
    return {
      diseaseSearchDialogVisible: false
    }
  },
  methods: {
    addDisease() {
      this.diseaseSearchDialogVisible = true;
    }
  }  
</script>
  • 子组件
vue
<template>
  <el-dialog
      v-model="dialogVisible"
      title="Tips"
      width="30%">
  </el-dialog>
</template>

<script>
export default {
  props: ['modelValue'],
  emits: ['update:modelValue'],
  computed: {
    dialogVisible: {
      get() {
        return this.modelValue;
      },
      set(value) {
        this.$emit('update:modelValue', value);
      }
    }
  }
}
</script>

emit通信

  • Counter.vue
vue
<template>
  <button @click="tap">点击计数</button>
</template>

<script>
export default {
  methods: {
    tap() {
      this.$emit("postCount")
    }
  },
  emits: ["postCount"]
}
</script>
  • App.vue
vue
<template>
  <div>点击次数 {{ total }}</div>
  <Counter @postCount="count"></Counter>
</template>

<script>
import Counter from "@/views/Counter.vue";

export default {
  data() {
    return {
      total: 0
    }
  },
  methods: {
    count() {
      this.total++;
    }
  },
  components: {Counter}
}
</script>

ref通信

  • MenuView.vue
vue
<template>
  <ul>
    <li v-for="item of list">{{item}}</li>
    <li><input v-model="newitem"><button @click="add">添加</button></li>
  </ul>
</template>

<script>
export default {
  name: "RefView",
  data() {
    return {
      newitem: "",
      list: [
          "宫保鸡丁",
          "鱼香肉丝",
          "京酱肉丝"
      ]
    }
  },
  methods: {
    add() {
      this.list.push(this.newitem);
    }
  }
}
</script>
  • App.vue
vue
<template>
  <MenuView ref="menuView"></RefView>
  <button @click="getChildProperty">点击获取子组件中的属性</button>
</template>

<script>
import MenuView from "@/views/MenuView.vue";

export default{
  methods: {
    getChildProperty() {
      console.log(this.$refs.menuView.list);
    }
  },
  components: {MenuView}
}
</script>

事件总线

不管在vue2还是在vue3中,都建议使用vuex来进行状态管理,而不是手动用事件总线来实现通信

Vue3从实例中移除了 $on$off$once 方法,保留了$emit方法

如果希望继续使用全局事件总线,可使用第三方库

Vue3官方推荐 mitt 或 tiny-emitter

建议在SFC(单文件组件)中使用

参考:

Vue3总线-mitt
挂载使用
  1. 安装

    bash
    pnpm i mitt
  2. 挂载

    • main.js
    js
    import { createApp } from 'vue'
    import App from './App.vue'
    import router from './router'
    import store from './store'
    
    import mitt from "mitt"
    
    let app = createApp(App);
    
    app.config.globalProperties.$bus = new mitt() 
    
    app.use(store);
    app.use(router);
    app.mount('#app');
  3. 发布消息

    js
    this.$bus.emit("event", "Hello");
  4. 订阅消息

    js
    this.$bus.on("event", (msg) => {
      console.log(msg);
    })
模块使用

注意

因为mitt中的总线,需要调用mitt()方法获取,每次获取到的总线都不是同一个对象

所以需要将mitt()的结果封装到一个js模块并导出,使用时再导入

  1. 安装

    bash
    pnpm i mitt
  2. 封装

    • bus/index.js
    js
    import mitt from 'mitt'
    const emitter = mitt()
    
    export default emitter
  3. 发布消息

    js
    import emitter from "@/bus"
    
    emitter.emit('event', 'Hello');
  4. 订阅消息

    js
    import emitter from "@/bus"
    
    // listen to an event
    emitter.on('event', msg => console.log(msg) )
    
    // listen to all events
    emitter.on('*', (type, e) => console.log(type, e) )
Vue3总线-tiny-emitter
挂载使用
  1. 安装

    bash
    pnpm i tiny-emitter
  2. 挂载

    js
    import { createApp } from 'vue'
    import App from './App.vue'
    import router from './router'
    import store from './store'
    
    import emitter from 'tiny-emitter'; 
    
    let app = createApp(App);
    
    app.config.globalProperties.$bus = new emitter() 
    
    app.use(store);
    app.use(router);
    app.mount('#app');
  3. 使用

    js
    this.$bus.on(...args);
    this.$bus.once(...args);
    this.$bus.off(...args);
    this.$bus.emit(...args);
模块使用
  1. 安装

    bash
    pnpm i tiny-emitter
  2. 使用

    js
    import emitter from 'tiny-emitter/instance'
    
    export default {
      $on: (...args) => emitter.on(...args),
      $once: (...args) => emitter.once(...args),
      $off: (...args) => emitter.off(...args),
      $emit: (...args) => emitter.emit(...args)
    }

插槽

匿名插槽

注意

匿名插槽的本质,是使用了默认命名default

vue
<template>
    <div>----------</div>
    <slot></slot>
    <div>----------</div>
</template>
vue
<template>
  <Swiper>
    <div class="swiper-part1">轮播第一张</div>
    <div class="swiper-part2">轮播第二张</div>
  </Swiper>
</template>

<script>
import Swiper from "@/views/Swiper.vue";

export default {
  components: {Swiper}
}
</script>

命名插槽

既具有名字的插槽,当插槽个数不止一个,那么到底将元素放到哪个插槽中,就需要通过名字指定

vue
<template>
  <div>----------</div>
  <slot>
  <div>----------</div>
  <slot name="second"></slot>
  <div>----------</div>
  <slot name="third"></slot>
  <div>----------</div>
</template>
vue
<template>
  <Swiper>
    <template v-slot:default>
      <span>插到第一个插槽</span>
    </template>

    <template v-slot:second>
      <span>插到第二个插槽</span>
    </template>

    <template v-slot:third>
      <span>插到第三个插槽</span>
    </template>    
  </Swiper>
</template>

<script>
import Swiper from "@/views/Swiper.vue";

export default {
  components: {Swiper}
}
</script>

缩写

vue
<template>
  <div>----------</div>
  <slot>
  <div>----------</div>
  <slot name="second"></slot>
  <div>----------</div>
  <slot name="third"></slot>
  <div>----------</div>
</template>
vue
<template>
  <Swiper>
    <template #default>
      <span>插到第一个插槽</span>
    </template>

    <template #second>
      <span>插到第二个插槽</span>
    </template>

    <template #third>
      <span>插到第三个插槽</span>
    </template>    
  </Swiper>
</template>

<script>
import Swiper from "@/views/Swiper.vue";

export default {
  components: {Swiper}
}
</script>

插槽传参

vue
<template>
<div>
  <slot :text="msg" :count="1"></slot>
</div>
</template>
vue
<template>
<Child>
  <template #default="scope">
    {{ scope.text }} {{ scope.count }}
  </template>
</Child>
</template>

动态组件【了解】

有时,我们可能需要根据状态来决定使用那个组件,比如下面的例子:

动态组件使用component标签,并且需要添加:is属性 保持组件的缓存,需要在component外边,添加keep-alive标签

javascript
Vue.createApp({
    template: `
        <div>
            <button @click="mine">我的</button>
            <button @click="cart">购物车</button><br/>
            <keep-alive>
                <component :is="activecomponent"></component>
            </keep-alive>
        </div>
    `,
    methods: {
        mine() {
            this.activecomponent = "mine";
        },
        cart() {
            this.activecomponent = "cart";
        }
    },
    data() {
        return {
            activecomponent: "mine"
        }
    },
    components: {
        mine: {
            template: `
                <div>
                    我的页
                </div>
            `
        },
        cart: {
            template: `
                <div>
                    购物车页
                    <ul>
                        <li v-for="cartname of cart">{{cartname}}</li>
                        <li><input ref="newprod"><button @click="addprod">添加菜品</button></li>
                    </ul>
                </div>
            `,
            data() {
                return {
                    cart: ["宫保鸡丁", "水煮肉片", "乾隆白菜"]
                }
            },
            methods: {
                addprod() {
                    this.cart.push(this.$refs.newprod.value);
                    this.$refs.newprod.value = "";
                }
            }
        }
    }
}).mount(".container");

异步组件【了解】

在实际开发中,一个应用可能会非常复杂。它可能会由很多组件组成。如果在应用启动时就加载所有组件,势必会造成效率低下。因此,正确的方式应该是按需加载。也就是先加载必要组件,然后根据需求在加载其它组件。

为了实现这个需求,Vue为我们提供了异步组件。

官网解释:在大型应用中,我们可能需要将应用分割成小一些的代码块,并且只在需要的时候才从服务器加载一个模块。为了简化,Vue 允许你以一个工厂函数的方式定义你的组件,这个工厂函数会异步解析你的组件定义。Vue 只有在这个组件需要被渲染的时候才会触发该工厂函数,且会把结果缓存起来供未来重渲染。

html
<body>
    <div id="app">
        <mycomponent></mycomponent>
    </div>

    <template id="mytemplate">
        <div>
            <subcomponents1></subcomponents1>
            <subcomponents2></subcomponents2>
        </div>
    </template>

    <script src="https://cdn.bootcss.com/vue/3.2.47/vue.global.min.js"></script>
    <script>
        Vue.createApp({
            components: {
                mycomponent: {
                    template: '#mytemplate',
                    data() {
                        return {}
                    },
                    components:{          //声明子组件
                        subcomponents1:{
                            template:`<div>第一个组件</div>`,
                        },
                         subcomponents2:Vue.defineAsyncComponent(()=>{
                            return new Promise((resolve,reject)=>{
                                setTimeout(() => {
                                    resolve({
                                        template:`<div>第二个组件</div>`,
                                    });
                                }, 3000);
                            })
                        })
                    }
                }
            }
        }).mount('#app');
    </script>
</body>
  • 上面代码中声明了两个子组件,一个是同步组件,一个是异步组件。
  • 使用 Vue.defineAsyncComponent 来声明异步组件,并且它要求返回一个Promise对象。
  • 代码运行后,先加载第一个子组件,3秒钟后加载第二个组件。

反向代理

配置

vue.config.js配置方案
javascript
module.exports = defineConfig({
  devServer: {
    proxy: {
      '/maoyan': {
        target: 'https://i.maoyan.com',
        changeOrigin: true,
        pathRewrite: {
          '^/maoyan': ''
        }
      }
    }
  }
}
  • vite.config.js
javascript
export default defineConfig({
  server: {
    proxy: {
      '/maoyan': {
        target: 'https://i.maoyan.com',
        changeOrigin: true,
        rewrite: path => path.replace(/^\/maoyan/, '')
      }
    }
  }
}

测试

查看以下地址的内容是否一致

路由

想要查看不同的页面,浏览器的地址也随之变得不同

快速入门

使用router模块

不使用vue-router系统自带模块

而是使用vue脚手架在本项目中创建的模块

  • main.js
javascript
import router from './router'
createApp(App).use(router).mount('#app')

配置router

  • router/index.js
javascript
// @的意图是本项目中的src目录
import CartView from '@/views/CartView'
import MineView from '@/views/MineView'

const routes = [
  {
    path: '/cart',
    name: 'cart',
    component: CartView
  },
  {
    path: '/mine',
    name: 'mine',
    component: MineView
  }
]

预留路由视图

vue
<template>
  <div>
    <button @click="toggleMine">我的页</button>
    <button @click="toggleCart">购物车页</button>
    <router-view></router-view>
  </div>
</template>

声明式路由

声明式路由标签

tex
router-link标签

标签伪装

在vue3中,router-link标签的tag属性已经被弃用

应使用以下方式替代:

--- https://router.vuejs.org/guide/migration/#removal-of-event-and-tag-props-in-router-link

html
<!-- vue2 -->
<router-link to="/about" tag="button">About Us</router-link>

<!-- vue3 -->
<router-link to="/about" custom v-slot="{ navigate }">
  <button @click="navigate" @keypress.enter="navigate" role="link">
    About Us
  </button>
</router-link>

标签高亮

  • 自定义高亮样式
html
<router-link to="/mine" active-class="active">
  我的页
</router-link>

<router-link to="/cart" active-class="active">
  购物车页
</router-link>
  • 伪装后高亮样式
html
<router-link to="/"
             custom
             v-slot="{ navigate, isActive, isExactActive }">
  <button @click="navigate"
          @keypress.enter="navigate"
          role="link"
          :class="[isActive && 'router-link-active', isExactActive && 'router-link-exact-active']">
    Home
  </button>
</router-link>|

编程式路由

javascript
export default {
  methods: {
    toggleMine () {
      this.$router.push("/mine");
    },
    toggleCart () {
      this.$router.push("/cart");
    }
  }
}

重定向

  • router/index.js
javascript
const routes = [
  {
    path: '/',
    redirect: '/mine'
  },
  {
    path: '/cart',
    name: 'cart',
    component: CartView
  },
  {
    path: '/mine',
    name: 'mine',
    component: MineView
  },
  {
    path: '/:pathMatch(.*)*',
    redirect: '/mine'
  }
]

二级路由

  • router/index.js
javascript
const routes = [{
  path: '/cinema',
  component: CinemaView,
  children: [{
    path: '/cinema',
    redirect: '/cinema/hot'
  },{
    path: '/cinema/hot',
    component: HotView
  },{
    path: '/cinema/coming',
    component: ComingView
  }]
}]

路由传参

注意

在版本号 4.1.4 (2022-08-22) 时,params形式的传参已被弃用,目前只能够使用query形式

https://github.com/vuejs/router/blob/main/packages/router/CHANGELOG.md#414-2022-08-22

英文文档解析

This change will break your app. This behavior has worked in some scenarios but has been advised against for years as it's an anti-pattern in routing for many reasons, one of them being reloading the page lose the params.

很多理由证明,这种路由是一种反模式。其中一个重要的理由是,重新加载页面会丢失参数。

命名路由

javascript
const routes = [
  {
    path: '/moviedetail',
    name: 'moviedetail',
    component: MovieDetailView
  }
]

路由传参-声明式路由

除了可以使用query之外,也可以使用params

但一定要注意,传参用query,接收也就必须用query。

params同上

vue
<template>
  <div>
    正在热映
    <ul>
      <li v-for="movie of hot" :key="movie.nm">
        <router-link :to='{name: "moviedetail", query: {id: movie.id}}'>
          {{ movie.nm }}
        </router-link>
      </li>
    </ul>
  </div>
</template>

路由传参-编程式路由

javascript
methods: {
  routejump() {
    this.$router.push({
      name: 'moviedetail',
      query: {id: '这个是编程式路由传的参'}
    });
  }
}

路由接参

javascript
export default {
  name: "MovieDetailView",
  mounted() {
    console.log(this.$route.query.id);
  }
}

路由守卫

全局守卫

javascript
router.beforeEach((to, from) => {
    let isLogin = false;
    if (to.name == 'profile' && !isLogin) {
        return {name: 'login', query: {from: to.name}};
    }
});

局部守卫

javascript
beforeRouteEnter(to, from) {
  let isLogin = false;
  if (!isLogin) {
    return {name: 'login', query: {from: 'profile'}};
  }
},

beforeRouteUpdate(to, from) {
},

beforeRouteLeave(to, from) {
}

历史模式【了解】

https://router.vuejs.org/zh/guide/essentials/history-mode.html

Vuex

  • Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式 + 库。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。

  • state中的数据,可以直接访问,但是不建议修改。

  • 创建store对象时,如果使用strict: true开启了严格模式,修改state中的数据会报错。如果要修改,使用mutations进行数据修改

  • mutations中定义的方法,参数有statevalue。其中state代表store中的state数据区,value则代表传过来的实参。使用this.$store.commit("setTabbarShow", false)可触发

  • getters可理解为vue组件中的计算属性,参数有state。其中state代表store中的state数据区。使用this.$store.getters.getTabbarShow可访问

  • actions可用于执行异步方法,参数有context。其中context代表store对象。使用this.$store.dispatch("requestData")可触发。虽然actionsstore对象的属性,但是修改state中的值,应仍旧使用commit触发mutation

状态管理模式

状态自管理应用包含以下几个部分

  • 状态,驱动应用的数据源;

  • 视图,以声明方式将状态映射到视图;

  • 操作,响应在视图上的用户输入导致的状态变化。

state

定义

javascript
import { createStore } from 'vuex'

export default createStore({
  state: {
    isTabbarShow: true
  },
  getters: {
  },
  mutations: {
  },
  actions: {
  },
  modules: {
  }
})

编程式引用

注意

路由守卫场景下,beforeRouteEnter中,无法使用this.$store引用vuex

此时可通过import导入store模块解决

javascript
import store from '@/store'

export default {
  name: "ProfileView",
  beforeRouteEnter(to, from) {
    if (!store.state.isLogin) {
      return {name: 'login', query: {to: 'profile'}};
    }
  }
}

声明式引用

html
<ul v-if="$store.state.isTabbarShow">
</ul>

mutations

javascript
import { createStore } from 'vuex'

export default createStore({
  strict: true,
  state: {
    isTabbarShow: true
  },
  getters: {
  },
  mutations: {
    setTabbarShow(state, value) {
      state.isTabbarShow = value;
    }
  },
  actions: {
  },
  modules: {
  }
})
vue
<template>
  <div>detail...</div>
</template>

<script>
export default {
  name: "MovieDetailView",
  mounted() {
    this.$store.commit("setTabbarShow", false);
  },
  unmounted() {
    this.$store.commit("setTabbarShow", true);
  }
}
</script>

<style scoped>

</style>

getters

注意

虽然getters的本质是一个方法,但是在引用时不能使用小括号,这也就意味着getters不能有任何参数

javascript
import { createStore } from 'vuex'

export default createStore({
  strict: true,
  state: {
    isTabbarShow: true
  },
  getters: {
    getTabbarShow(state) {
      return state.isTabbarShow;
    }
  },
  mutations: {
    setTabbarShow(state, value) {
      state.isTabbarShow = value;
    }
  },
  actions: {
  },
  modules: {
  }
})
vue
<ul v-if="$store.getters.getTabbarShow">
</ul>

actions

javascript
import { createStore } from 'vuex'
import axios from 'axios'

export default createStore({
  strict: true,
  state: {
    isTabbarShow: true,
    hot: []
  },
  getters: {
    getTabbarShow(state) {
      return state.isTabbarShow;
    }
  },
  mutations: {
    setTabbarShow(state, value) {
      state.isTabbarShow = value;
    },
    setHot(state, value) {
      state.hot = value;
    }
  },
  actions: {
    requestData(context) {
      axios.get('/maoyan/api/mmdb/movie/v3/list/hot.json?ct=%E5%A4%A7%E8%BF%9E&ci=65&channelId=4')
          .then(res => {
            context.commit('setHot', res.data.data.hot);
          })
    }
  },
  modules: {
  }
})
vue
<template>
  <div>
    正在热映
    <ul v-if="$store.state.isTabbarShow">
      <li @click="routejump">编程式路由...</li>
      <li v-for="movie of $store.state.hot" :key="movie.nm">
        <router-link :to='{name: "moviedetail", query: {id: movie.id}}'>
          {{ movie.nm }}
        </router-link>
      </li>
    </ul>
  </div>
</template>

<script>
import axios from 'axios'

export default {
  name: "HotView",
  data() {
    return {
      hot: []
    }
  },
  methods: {
    routejump() {
      this.$router.push({
        name: 'moviedetail',
        query: {id: '这个是编程式路由传的参'}
      });
    }
  },
  mounted() {
    this.$store.dispatch('requestData');
  }
}
</script>

<style scoped>

</style>

mapState风格【了解】

https://vuex.vuejs.org/zh/guide/state.html#mapstate-辅助函数

mutations常量风格【了解】

https://vuex.vuejs.org/zh/guide/mutations.html#使用常量替代-mutation-事件类型

pinia

定义

注意

以下代码已存在

  • store/counter.js
js
import { ref, computed } from 'vue'
import { defineStore } from 'pinia'

export const useCounterStore = defineStore('counter', () => {
  const count = ref(0)
  const doubleCount = computed(() => count.value * 2)
  function increment() {
    count.value++
  }

  return { count, doubleCount, increment }
})

使用

vue
<template>
    <h1>count: {{ counterStore.count }}</h1>
	<h1>double count: {{ counterStore.doubleCount }}</h1>
	<button @click="counterStore.increment()"> +1 </button>
</template>

<script setup>
import { useCounterStore } from '../stores/counter'
const counterStore = useCounterStore()
</script>

与vuex对比

  • ref <==> state

  • computed <==> getters

  • function <==> actions/mutations

总结

https://vuejs.org/api/

https://cn.vuejs.org/api/

  • 内部指令
tex
v-if
v-else-if
v-else
v-show
v-for
v-text
v-html
v-model
v-bind/:
v-on/@
{{}}
v-slot      <- 只在template标签才能够使用的指令,用于指定插槽名
  • 全局 API
tex
el           <- 挂载到哪个元素上,在vue3时不再建议使用
data         <- 数据区,vue2时是obj类型;vue3时是function类型
methods      <- 方法区,obj类型
computed     <- 计算属性,obj类型,只会初始化一次,而且是第一次使用时才会计算。
             // 例外情况为:假如在计算属性方法内部,有其他属性参与运算
             // 则只要属性变更了,计算属性方法就会再执行一次,而不是使用缓存值
watch        <- 监听器,obj类型,监听某个属性的变化
template     <- 用于替换元素的outerHTML,obj类型
components   <- 定义组件,obj类型
props        <- 用于父组件用v-bind传输属性的,obj类型
name         <- 组件名称,String类型
钩子函数     <- 一共有8个
路由守卫函数 <- 一共3个(beforeRouteEnter, beforeRouteUpdate, beforeRouteLeave)
             // 参数有2个,分别是to和from,
             // 如果想要跳转到其他的页面,可返回一个对象
             // 此对象有两个属性: name、params或query...
  • 内置属性
tex
$emit
$on
$refs
$router.push({name: "...", params: {...}, query: {...})
$router.replace({name: "...", params: {...}, query: {...})
$router.go(数字)
$route.params
$route.query
$store.state.prop
$store.getters.prop
$store.commit("fn", value)
$store.dispatch("fn")
  • 新标签
tex
<keep-alive>
    <component :is="activeComponent"></component>
</keep-alive>

<template v-slot:someSlot></template>

<router-view></router-view>

<router-link to="/" custom  v-slot="{ navigate }" active-class='active'>
  <button @click="navigate" @keypress.enter="navigate" role="link">
    Go to Home
  </button>
</router-link>

附录

IDEA窗口禁止运行脚本的问题

以管理员身份运行PowerShell

执行以下命令,根据提示,输入A,回车

set-ExecutionPolicy RemoteSigned

日期格式转换工具monent

安装

bash
pnpm i moment

使用

vue
<template>
    <el-form-item label="出生日期">
        <el-date-picker
            v-model="form.birthday"
            type="date"
            placeholder="选择日期"
        />
      </el-form-item>
</template>

<script>
let d1 = moment(this.form.birthday).format("yyyy-MM-DD");
let d2 = moment().format("yyyy-MM-DD");
</script>

参考资料

vue3知识地图

参考网站

https://cn.vuejs.org/guide/introduction.html

https://router.vuejs.org/zh/guide/#html

https://router.vuejs.org/zh/guide/essentials/history-mode.html

https://cli.vuejs.org/zh/config/#devserver-proxy

https://vuex.vuejs.org/zh/

https://space.bilibili.com/165659472

https://jspang.com/

https://www.npmjs.com/package/vue

https://cdnjs.com

https://www.jsdelivr.com/

https://cdn.bootcss.com/