Skip to content

Vue 脚手架笔记

第一章:Vue CLI

一、认识 Vue CLI

1. 是什么?

Vue CLI 是 Vue.js 的官方命令行工具,它提供了一种快速搭建 Vue.js 项目的方法。Vue CLI 提供了一套完整的工具链,包括项目脚手架、热重载、代码压缩、单元测试、端到端测试等。

2. 如何使用

要使用 Vue CLI,首先需要在系统上安装 Node.js 和 npm,然后通过 npm 安装 Vue CLI:

bash
npm install -g @vue/cli

然后,就可以使用 vue create 命令创建一个新的 Vue.js 项目:

bash
vue create my-project

二、脚手架文件结构

text
  ├── node_modules
  ├── public: 存放一些不需要经过 webpack 处理的静态资源文件
  │   ├── favicon.ico: 页签图标
  │   └── index.html: 主页面
  ├── src
  │   ├── assets: 存放静态资源
  │   │   └── logo.png
  │   │── component: 存放组件
  │   │   └── HelloWorld.vue
  │   │── App.vue: 汇总所有组件
  │   │── main.js: 入口文件
  ├── .gitignore: git版本管制忽略的配置
  ├── jsconfig.json: 给 VSCode 的配置文件, 优化代码提示
  ├── babel.config.js: babel的配置文件
  ├── package.json: 应用包配置文件
  ├── package-lock.json:包版本控制文件
  ├── README.md: 应用描述文件

三、关于不同版本的 Vue

  1. vue.js 与 vue.runtime.xxx.js 的区别:

    ① vue.js 是完整版的 Vue,包含:核心功能 + 模板解析器。

    ② vue.runtime.xxx.js 是运行版的 Vue,只包含:核心功能;没有模板解析器。

  2. 因为 vue.runtime.xxx.js 没有模板解析器,所以不能使用 template 这个配置项,需要使用 render 函数接收到的 createElement(也叫 h 函数) 函数去指定具体内容。

开发阶段:
  编写 template ━━━━━━━━━━━━━━┓

运行阶段(浏览器):            ↓
  template 字符串 ━━━━━━━→ 模板编译器 ━━━━━━━→ render 函数 ━━━━━━━→ 虚拟 DOM ━━━━━━━→ 真实 DOM

缺点:体积大 + 性能开销(运行时编译)

==============================================================================================

开发阶段(构建工具):
  .vue 文件 ━━━━━━━→ Webpack/Vite 编译 ━━━━━━━━→ render 函数

运行阶段(浏览器):                                   ↓
  render 函数 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━→ 虚拟 DOM ━━━━━━━→ 真实 DOM

缺点:体积小 + 性能好(无需运行时编译)

四、vue.config.js 配置文件

1. 是什么

使用 vue inspect > output.js 仅仅可以查看到 Vue 脚手架的默认配置。想修改默认配置怎么办?

vue.config.js 是一个可选的配置文件,如果项目的 (和 package.json 同级的) 根目录中存在这个文件,那么它会被 @vue/cli-service 自动加载。也可以使用 package.json 中的 vue 字段。

javascript
module.exports = {
  pages: {
    index: {
      // page 的入口
      entry: 'src/index/main.js',
      // 模板来源
      template: 'public/index.html',
      outputDir: 'dist',
      // 在 dist/index.html 的输出
      filename: 'index.html',
      /*
         当使用 title 选项时,template 中的 title 标签需要是 <title><%= htmlWebpackPlugin.options.title %></title>
         <title><%= htmlWebpackPlugin.options.title %></title> 表示读取 package.json 中 name 选项的值作为网页 title 标签内容
      */
      title: 'Index Page'
    }
  },
  configureWebpack: {
    resolve: {
      alias: {
        // @ 是已经在内部帮我们配置好的别名,指代 src 目录
        "utils": "@/utils"
      }
    }
  },
  lintOnSave: false // 关闭 eslint 等代码书写格式检查
}

详情见:配置参考

2. devServer.proxy

Vue 脚手架配置代理。

方法一

在 vue.config.js 中添加如下配置:

javascript
module.exports = {
  devServer: {
    proxy: 'http://localhost:5000'
  }
}

说明:

1)工作方式:若按照上述配置代理,当请求了前端不存在的资源时,那么该请求会转发给服务器 (优先匹配前端资源)。

2)优点:配置简单,请求资源时直接发给前端(8080)即可。

    缺点:不能配置多个代理,不能灵活的控制请求是否走代理。

方法二

编写 vue.config.js 配置具体代理规则:

javascript
module.exports = {
  devServer: {
    proxy: {
      '/api1': { // 匹配所有以 '/api1'开头的请求路径
        target: 'http://localhost:5000', // 代理目标的基础路径
        changeOrigin: true,
        pathRewrite: {'^/api1': ''}
      },
      '/api2': {// 匹配所有以 '/api2'开头的请求路径
        target: 'http://localhost:5001', // 代理目标的基础路径
        changeOrigin: true,
        pathRewrite: {'^/api2': ''}
      }
    }
  }
}

/*
  changeOrigin 设置为 true 时,服务器收到的请求头中的 host 为:localhost:5000
  changeOrigin 设置为 false 时,服务器收到的请求头中的 host 为:localhost:8080
  changeOrigin 默认值为 true
*/

优点:可以配置多个代理,且可以灵活的控制请求是否走代理。

缺点:配置略微繁琐,请求资源时必须加前缀。

第二章:深入组件

一、VC 常用属性与方法

1. 属性

1)$refs

(1) ref 被用来给元素或子组件注册引用信息(id 的替代者)。

(2) 应用在 html 标签上获取的是真实 DOM 元素;应用在组件标签上是组件实例对象(vc)。若想获取到子组件真实的解析后的 HTML 标签,需要在组件标签上添加 id 属性,在 Vue 中使用 JavaScript 原生方式获取。

(3) 使用方式

      第一步:打标识 <h1 ref="xxx">.....</h1><School ref="xxx"></School>

      第二步:获取 this.$refs.xxx

注意:v-for 循环出来之后,多个元素有相同的 ref 值,this.$refs.xxx 找到全部。

vue
<script>
export default {
  data() {
    return {
      list: [
        /* ... */
      ]
    }
  },
  mounted() {
    console.log(this.$refs.items)
  }
}
</script>

<template>
  <ul>
    <!-- 当在 v-for 中使用模板引用时, 相应的引用中包含的值是一个数组 -->
    <li v-for="item in list" ref="items">
      {{ item }}
    </li>
  </ul>
</template>
2)$el

$refs 应用在组件标签上是组件实例对象(vc)。想进一步获取到 DOM 对象,可以使用 $el。

3)$parent 和 $root

$parent:获取父组件。

$root:获取根组件。

注意:在 Vue3 中已经移除了 $children 属性(获取当前组件的所有直接子组件实例,返回数组形式),所以不可以使用了。

2. nextTick 方法

1)语法:this.$nextTick(回调函数)

2)作用:在下一次 DOM 更新结束后执行其指定的回调。

3)解释:数据是同步更新,视图是异步更新。因为如果视图是同步更新的,那会导致多次渲染,浪费不必要的性能。

4)使用场景:

  • 如果想要在修改数据后立刻得到更新后的 DOM 结构,可以使用 this.$nextTick()
  • 在 created 生命周期中进行 DOM 操作。

大白话:当改变数据后,要基于更新后的新 DOM 进行某些操作时,要在 nextTick 所指定的回调函数中执行。

二、组件间的通信

1. 父给子传

说明

父给子传信息,使用 props 配置项。

1)基本使用

(1) 功能:让组件接收外部传过来的数据。

(2) 传递数据:<Demo :name="xxx" age="18" />

(3) 接收数据

第一种方式(只接收):props:['name']

第二种方式(限制类型):props:{name:String}

第三种方式(限制类型、限制必要性、指定默认值)

javascript
props:{
  name:{
    type: String, // 类型
    required: true, // 必要性
    default: '老王', // 默认值

    // 自定义类型校验函数
    validator(value) {
      // 自定义校验逻辑
      return 是否通过校验
    }
  }
}

这样在接受数据的组件实例上就会有属性了。

(4) type 支持的类型:Number、Boolean、String、Symbol、Array、Date、Object、Function。

(5) 注意:① 如果是 props 值为对象或数组,那么 default 必须写为函数。② 若 props: ['cuiFQ'],在组件传递数据可以写为 <XXX cuiFQ="XXX" /><XXX cui-f-q="XXX" />

备注:① props 是只读的,Vue 底层会监测你对 props 的修改,如果进行了修改,就会发出警告。若业务需求确实需要修改,那么请复制 props 的内容到 data 中一份,然后去修改 data 中的数据。② props 与 data 中的数据发生冲突,会优先使用 props 中的数据,且 Vue 会在控制台发出警告。

2)非 Prop 的 Attribute

什么是非 Prop 的 Attribute 呢?

当我们传递给一个组件某个属性,但是该属性没有在定义对应 props 或者 emits 时, 就称之为非 Prop 的 Attribute。常见的包括 class、style、id 属性等。

Attribute 继承?

当组件有单个根节点时, 非 Prop 的 Attribute 将会自动添加到底层节点的 Attribute 中。

禁用 Attribute 继承

如果我们不希望组件的根元素继承 attribute, 可以在组件中设置 inheritAttrs: false;

不采用 attribute 继承的常见情况是需要将 attribute 应用于根元素之外的其他元素。我们可以通过 $attrs 来访问所有非 props 的 attribute。

多根节点

多个根节点的 attribute 如果没有显示的绑定,那么会报警告,我们必须手动的指定要绑定到哪一个属性上。

vue
<script setup>
defineProps(['label', 'value'])
defineEmits(['input'])
defineOptions({
  inheritAttrs: false
})
</script>

<template>
  <label>
    {{ label }}
    <input
      v-bind="$attrs"
      v-bind:value="value"
      v-on:input="$emit('input', $event.target.value)"
    />
  </label>
</template>

2. 子给父传

方法一:props + 回调

A 组件是 B 的父组件。现在 B 要给 A 传递数据,怎么办?

思路:在 A 组件编写一个函数,通过 props 传递给 B,然后在 B 合适的位置调用 A 组件传过来的函数,这样 A 就能拿到 B 的数据了。

忠告

  1. 组件化编码流程

    (1). 拆分静态组件:组件要按照功能点拆分,命名不要与 html 元素冲突。

    (2). 实现动态组件:考虑好数据的存放位置,数据是一个组件在用,还是一些组件在用。

    ​ 1) 一个组件在用:放在组件自身即可。

    ​ 2) 一些组件在用:放在他们共同的父组件上(状态提升)。

    (3). 实现交互:从绑定事件开始。

  2. props 适用于

    (1). 父组件 ==> 子组件通信

    (2). 子组件 ==> 父组件通信(要求父先给子一个函数)

  3. 使用 v-model 时要切记:v-model 绑定的值不能是 props 传过来的值,因为 props 是不可以修改的(为了遵循单向数据流)!

  4. props 传过来的若是对象类型的值,修改对象中的属性时 Vue 不会报错,但不推荐这样做。

方法二:自定义事件

1)一种组件间通信的方式,适用于:子组件 ==> 父组件

2)使用场景:A 是父组件,B 是子组件,B 想给 A 传数据,那么就要在 A 中给 B 绑定自定义事件(事件的回调在 A 中)。

3)绑定自定义事件

  • 第一种方式:在父组件中使用的子组件标签上添加 v-on:自定义事件名 或者 @自定义事件名,例如:<Demo @atXQ="test" /><Demo v-on:atXQ="test" />(Vue 会把自定义事件名添加到当前子组件 VC 上)。然后在父组件的 methods 中编写一个函数,函数名为自定义事件名等号后面的取值。最后在子组件合适位置调用 this.$emit('自定义事件名',数据)

  • 第二种方式:在父组件中使用的子组件标签上添加 ref 属性,这样在父组件 VC 的 $refs 属性上就会有子组件 VC。接着在父组件的 mounted 钩子上拿到子组件 VC,给子组件 VC 绑定一个组件自定义事件和当子组件自定义事件触发时要执行的父组件函数。最后在子组件合适位置调用 this.$emit('自定义事件名',数据)

    javascript
    <Demo ref="xxx" />
    
    ......
    
    methods(){
      test(val){......}
    }
    mounted(){
      this.$refs.xxx.$on('atXQ',this.test)
    }

4)其他

  • 若想让自定义事件只能触发一次,可以使用 once 修饰符,或 $once 方法。
  • 触发自定义事件:this.$emit('atXQ',数据)
  • 解绑自定义事件:this.$off('atXQ')this.$off(['atXQ','atCui'])this.$off() 解绑所有自定义事件。
  • 组件上也可以绑定原生 DOM 事件,需要使用 native 修饰符。

注意:通过 this.$refs.xxx.$on('atXQ',回调) 绑定自定义事件时,回调要么配置在 methods 中要么用箭头函数,否则 this 指向会出问题!

3. 任意组件间通信

[1] 全局事件总线

兄弟之间用总线,称为全局事件总线(Global Event Bus)。说白了,也是用的自定义事件。

1)一种组件间通信的方式,适用于任意组件间通信

2)如何实现?

  1. 安装全局事件总线

    javascript
    new Vue({
      // ......
      beforeCreate() {
        Vue.prototype.$bus = this // 安装全局事件总线,$bus就是当前应用的vm
      },
      // ......
    })
  2. 使用事件总线

    接收数据:A 组件想接收数据,则在 A 组件中给 $bus 绑定自定义事件,事件的回调留在 A 组件自身。

    javascript
    methods(){
      demo(data){......}
    }
    // ......
    mounted() {
      this.$bus.$on('xxxx',this.demo)
    }

    提供数据:this.$bus.$emit('xxxx',数据)

小贴士:最好在 beforeDestroy 钩子中,用 $off 去解绑当前组件所用到的事件。

[2] 消息订阅与发布 (pubsub)

1)一种组件间通信的方式,适用于任意组件间通信

2)使用步骤

  1. 安装 pubsub:npm i pubsub-js

  2. 引入:import pubsub from 'pubsub-js'。引入的 pubsub 是一个对象,上面有很多实用方法。

  3. 使用 pubsub 对象

    接收数据:A 组件想接收数据,则在 A 组件中订阅消息,订阅的回调留在 A 组件自身。

    javascript
    methods(){
      demo(data){......}
    }
    // ......
    mounted() {
      this.pid = pubsub.subscribe('xxx',this.demo) // 订阅消息
    }

    提供数据:pubsub.publish('xxx',数据)

小贴士:最好在 beforeDestroy 钩子中,用 PubSub.unsubscribe(pid) 来取消订阅。

[3] 依赖注入 Provide 和 Inject

provide 和 inject 是用于在组件树中共享数据的 API。它们允许一个祖先组件提供数据,并让其后代组件注入和使用这些数据,而不需要通过 props 一层一层地传递。

注意:(前提是 Vue2) 如果是对象类型的数据,则为响应式;非对象类型的数据为非响应式。

使用 provide 提供数据

在祖先组件中,使用 provide 来提供数据。provide 是一个函数,它返回一个包含要提供的数据的对象。

vue
<template>
  <div>
    <ChildComponent />
  </div>
</template>

<script>
import { provide } from 'vue';

export default {
  name: 'ParentComponent',
  setup() {
    const message = 'Hello from ParentComponent';

    // 使用 provide 提供数据
    provide('message', message);

    return {};
  },
};
</script>
使用 inject 注入数据

在后代组件中,使用 inject 来注入数据。inject 是一个函数,它接收一个键名或一个键名数组,并返回对应的提供的数据。

vue
<template>
  <div>
    Injected message: {{ message }}
  </div>
</template>

<script>
import { inject } from 'vue';

export default {
  name: 'ChildComponent',
  setup() {
    // 使用 inject 注入数据
    const message = inject('message');

    return {
      message,
    };
  },
};
</script>
注意事项

响应式数据:如果你想提供响应式数据,可以使用 Vue 的 ref、reactive、computed 来创建响应式对象,然后通过 provide 提供。

javascript
import { provide, ref } from 'vue';

export default {
  setup() {
    const message = ref('Hello from ParentComponent');
    provide('message', message);
  },
};

默认值:如果注入的键在祖先组件中没有提供,可以为 inject 提供一个默认值。

javascript
const message = inject('message', 'Default message');
Vue 2.x 示例
vue
<!-- ParentComponent.vue -->
<template>
  <div>
    <ChildComponent />
  </div>
</template>

<script>
import ChildComponent from './ChildComponent.vue';

export default {
  name: 'ParentComponent',
  components: {
    ChildComponent,
  },
  provide() {
    return {
      message: 'Hello from ParentComponent', // 非响应式
      obj: {} // 响应式
    };
  }
};
</script>

<!-- ChildComponent.vue -->
<template>
  <div>
    Injected message: {{ message }}
  </div>
</template>

<script>
export default {
  name: 'ChildComponent',
  inject: ['message'],
};
</script>

三、插槽 Slots

1)作用:让父组件可以向子组件的指定位置插入 html 结构。也是一种组件间通信的方式,适用于父组件 ==> 子组件。

2)分类:默认插槽、具名插槽、作用域插槽。

3)使用方式

[1] 默认插槽

vue
父组件中:
        <Category>
          <!-- 默认插槽内容 -->
          <div>html结构1</div>
        </Category>

子组件 Category 中:
        <template>
          <div>
            <!-- 定义插槽 -->
            <slot>插槽默认内容...</slot>
          </div>
        </template>

如果插槽是默认插槽,那么在使用的时候 v-slot:default="slotProps" 可以简写为 v-slot="slotProps"。并且如果只使用默认插槽,可以省略 template 标签,直接写在组件上。

vue
<Category v-slot="slotProps">
  <!-- 默认插槽内容 -->
  <span>{{ slotProps.item }} - {{ slotProps.index }}</span>
</Category>

<!-- 可以使用 es6 语法简化代码 -->
<Category v-slot="{ item, index }">
  <span>{{ item }} - {{ index }}</span>
</Category>

[2] 具名插槽

v-slot:footer 只能写在 template 标签中,简写为 #。

vue
父组件中:
        <Category>
          <template slot="center">
            <div>html结构1</div>
          </template>

          <template v-slot:footer>
            <div>html结构2</div>
          </template>
          <!-- 简写语法 # -->
          <template #footer>
            <div>html结构2</div>
          </template>
      </Category>

子组件 Category 中:
        <template>
          <div>
            <!-- 定义插槽 -->
            <slot name="center">插槽默认内容...</slot>
            <slot name="footer">插槽默认内容...</slot>
          </div>
        </template>

动态插槽名(了解)

vue
父组件中:
<template>
  <Category>
    <template v-slot:[dynamicSlotName]>
      <!-- 插槽内容 -->
    </template>
  </Category>
</template>

<script>
export default {
  data: function(){
    return {
      // 可以动态的改变这个值, 来决定插槽在组件中的位置
      dynamicSlotName: "center"
    }
  }
}
</script>

[3] 作用域插槽

① 理解:数据在组件的自身,但根据数据生成的结构需要组件的使用者来决定

② 具体编码:

games 数据在 Category 组件中,但使用数据所遍历出来的结构由 App 组件决定。

vue
App 父组件中:
      <Category>
        <template scope="scopeData">
          <!-- 生成的是ul列表 -->
          <ul>
            <li v-for="g in scopeData.games" :key="g">{{g}}</li>
          </ul>
        </template>
      </Category>

      <Category>
        <template slot-scope="scopeData">
          <!-- 生成的是h4标题 -->
          <h4 v-for="g in scopeData.games" :key="g">{{g}}</h4>
        </template>
      </Category>

Category 子组件中:
        <template>
          <div>
            <slot :games="games"></slot>
          </div>
        </template>
      
        <script>
          export default {
            name:'Category',
            props:['title'],
            //数据在子组件自身
            data() {
              return {
                games:['红色警戒','穿越火线','劲舞团','超级玛丽']
              }
            },
          }
        </script>

旧版语法

上面的插槽语法是新版语法,在 Vue 3.x 依然可以使用。但插槽有旧版语法,只能在 Vue 2.x 中使用。
具体参见 Vue 2.x 插槽
在 2.6.0 中,我们为具名插槽和作用域插槽引入了一个新的统一的语法 (即 v-slot 指令)。它取代了 slotslot-scope 这两个目前已被废弃但未被移除且仍在文档中的 attribute。新语法的由来可查阅这份 RFC

四、Vuex

1. 初识 Vuex

Vuex 目前现状:Vuex 进入维护模式了

1)概念

在 Vue 中实现集中式状态(数据)管理的一个 Vue 插件,对 vue 应用中多个组件的共享状态进行集中式的管理(读 / 写),也是一种组件间通信的方式,且适用于任意组件间通信。

vuex 存储的数据是非持久化的

2)何时使用?
  • 多个视图依赖于同一状态。
  • 来自不同视图的行为需要变更同一状态。

总结:多个组件需要共享数据时。

3)工作原理

Vue Components 可以直接调用 Mutations;Actions、Mutations、State 都需要 Store 管理。

小贴士:actions 的存在是将业务逻辑从组件中抽取出来;mutations 是只负责更新 state,因此不能执行异步操作;state 是存储状态的仓库。

2. 快速入门

1)搭建 vuex 环境

说明

Vue2 中要用 Vuex3 版本;Vue3 中要用 Vuex4 版本。

① 创建文件:src/store/index.js

javascript
// 1. 引入
// 引入Vue核心库
import Vue from 'vue'
// 引入Vuex
import Vuex from 'vuex'
// 应用Vuex插件
Vue.use(Vuex)

// 2. 准备三个组件
// 2.1 准备actions对象——响应组件中用户的动作
//actions: 处理action,可以书写自己的业务逻辑,也可以处理异步
const actions = {}
// 2.2 准备mutations对象——修改state中的数据
// mutations: 修改state的唯一手段
const mutations = {}
// 2.3 准备state对象——保存具体的数据
// state: 仓库存储数据的地方
const state = {}

// 3. 创建并暴露store
export default new Vuex.Store({
  actions,
  mutations,
  state
})

② 在 main.js 中创建 vm 时传入 store 配置项

javascript
......
// 引入store
import store from './store'
......

// 创建vm
new Vue({
  el:'#app',
  render: h => h(App),
  store
})

这样就会在每个 vm 和 vc 上有 $store 属性了。

2)使用

① 初始化数据:配置 actions、配置 mutations。下面是在文件 @/store/index.js 中进行编写的。

javascript
// 引入Vue核心库
import Vue from 'vue'
// 引入Vuex
import Vuex from 'vuex'
// 引用Vuex
Vue.use(Vuex)

const actions = {
  // 响应组件中加的动作
  // context 是 miniStore; value 是调用 dispatch 方法传入的第二个参数
  jia(context,value){
    // console.log('actions中的jia被调用了',context,value)
    context.commit('JIA',value)
  },
}

const mutations = {
  // 执行加
  JIA(state,value){
    // console.log('mutations中的JIA被调用了',state,value)
    state.sum += value
  }
}

// 初始化数据
const state = {
  sum:0
}

// 创建并暴露store
export default new Vuex.Store({
  actions,
  mutations,
  state,
})

② 组件中使用

组件中读取 vuex 中的数据:$store.state.sum

组件中修改 vuex 中的数据:this.$store.dispatch('action中的方法名',数据)this.$store.commit('mutations中的方法名',数据)

备注:若没有网络请求或其他业务逻辑,组件中也可以越过 actions,即不写 dispatch,直接编写 commit。

3. 基本使用

1)getters 的使用

[1] 概念:当 state 中的数据需要经过加工后再使用时,可以使用 getters 加工。

[2] 使用步骤

① 在 store.js 中追加 getters 配置。

javascript
// ......
// getters: 理解为计算属性,用于简化仓库数据,让组件获取仓库的数据更加方便
const getters = {
  bigSum(state){
    return state.sum * 10
  }
}

// 创建并暴露store
export default new Vuex.Store({
  // ......
  getters
})

② 组件中读取数据:$store.getters.bigSum

2)辅助函数

① mapState 方法:用于帮助我们映射 state 中的数据为计算属性。

javascript
import {mapState} from 'vuex'

// ......

computed: {
  /*
     借助mapState生成计算属性(对象写法):sum、school、subject
     对象写法的格式:输出类似:{ 计算属性名: '仓库属性名' }
     返回值:
       {
         sum: function mappedState() { return this.$store.state.sum; },
         school: function mappedState() { return this.$store.state.school; },
         subject: function mappedState() { return this.$store.state.subject; }
       }
  */
  ...mapState({sum:'sum',school:'school',subject:'subject'}),
         
  // 借助mapState生成计算属性(数组写法):sum、school、subject
  ...mapState(['sum','school','subject']),
},

② mapGetters 方法:用于帮助我们映射 getters 中的数据为计算属性

javascript
import {mapGetters} from 'vuex'

......

computed: {
  /*
     借助mapGetters生成计算属性:bigSum(对象写法)
     对象写法的格式:{ 计算属性名: 'getters中的名称' }
     返回值:
       {
         bigSum: function mappedGetter() { return this.$store.getters.bigSum; }
       }
  */
  ...mapGetters({bigSum:'bigSum'}),

  // 借助mapGetters生成计算属性:bigSum(数组写法)
  ...mapGetters(['bigSum'])
},

③ mapActions 方法:用于帮助我们生成与 actions 对话的方法,即:包含 this.$store.dispatch(xxx) 的函数。

javascript
import {mapActions} from 'vuex'

......

methods:{
  // 靠mapActions生成(对象形式):incrementOdd、incrementWait
  ...mapActions({incrementOdd:'jiaOdd',incrementWait:'jiaWait'})

  // 靠mapActions生成(数组形式):jiaOdd、jiaWait
  ...mapActions(['jiaOdd','jiaWait'])
}

自动生成的方法:

js
// ...mapActions({incrementOdd:'jiaOdd',incrementWait:'jiaWait'})

incrementOdd(value){
  this.$store.commit('jiaOdd', value);
}

incrementWait(value){
  this.$store.commit('jiaWait', value);
}

④ mapMutations 方法:用于帮助我们生成与 mutations 对话的方法,即:包含 this.$store.commit(xxx) 的函数

javascript
import {mapMutations} from 'vuex'

......

methods:{
  // 靠mapActions生成(对象形式):increment、decrement
  ...mapMutations({increment:'JIA',decrement:'JIAN'}),
    
  // 靠mapMutations生成(数组形式):JIA、JIAN
  ...mapMutations(['JIA','JIAN']),
}

注意:mapActions 与 mapMutations 使用时,若需要传递参数,则要在模板中绑定事件时传递好参数,否则参数是事件对象。

3)模块化 + 命名空间

[1] 目的:让代码更好维护,让多种数据分类更加明确。

[2] 使用

① 修改 store.js

javascript
const countAbout = {
  namespaced: true, // 开启命名空间
  state: {x:1},
  mutations: { ... },
  actions: { ... },
  getters: {
    bigSum(state){
      return state.sum * 10
    }
  }
}

const personAbout = {
  namespaced: true, // 开启命名空间
  state: { ... },
  mutations: { ... },
  actions: { ... }
}

const store = new Vuex.Store({
  modules: {
    countAbout,
    personAbout
  }
})

推荐把 countAbout 与 personAbout 抽取到单独的 JavaScript 文件中(例如:countAbout 抽取到 count.js 文件中;personAbout 抽取到 personAbout .js 文件中)。然后使用 import 导入。有利于后期维护。

② 开启命名空间后,组件中读取 state 数据

javascript
// 方式一:自己直接读取
this.$store.state.personAbout.list
// 方式二:借助mapState读取
...mapState('personAbout',['list','total','city']),

③ 开启命名空间后,组件中读取 getters 数据

javascript
// 方式一:自己直接读取
this.$store.getters['personAbout/firstPersonName']
// 方式二:借助mapGetters读取
...mapGetters('personAbout',['firstPersonName'])

④ 开启命名空间后,组件中调用 dispatch

javascript
// 方式一:自己直接dispatch
this.$store.dispatch('personAbout/addPersonWang', person)
// 方式二:借助mapActions
...mapActions('personAbout',{addWang:'addPersonWang',jianWang:'delPersonWang'})

⑤ 开启命名空间后,组件中调用 commit

javascript
// 方式一:自己直接commit
this.$store.commit('personAbout/ADD_PERSON', person)
// 方式二:借助mapMutations
...mapMutations('personAbout',{jia:'ADD_PERSON', jian:'DEL_PERSON'}),

五、Pinia

1. 初识 Pinia

1)是什么?

Pinia 是由 Eduardo San Martin Morote 创建的,他是 Vue.js 核心团队的成员,负责 Vue Router 的维护。而 Vuex 是由 Vue.js 的创始人尤雨溪创建的。Pinia 是作为 Vuex 的一个轻量级替代方案而创建的,它提供了更简单和灵活的状态管理。

起始于 2019 年 11 月左右的一次实验,其目的是设计一个拥有组合式 API 的 Vue 状态管理库。但不强制要求开发者使用组合式 API。

2)优势
  • mutation 已被弃用。

  • 不再有可命名的模块。

  • 不再有嵌套结构的模块。在 Pinia 中,所有的 store 都是扁平的。

  • API 的设计方式是尽可能地利用 TS 类型推理。

  • 无需要动态添加 Store,它们默认都是动态的。

3)工作原理

2. 快速入门

1)安装

bash
npm install pinia
# 或者使用 yarn
yarn add pinia

2)集成 pinia

@/main.js 文件添加 pinia。

javascript
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'

const pinia = createPinia()
const app = createApp(App)

app.use(pinia)
app.mount('#app')

3. 核心概念

1)store
[1] 定义store

Option Store

javascript
export const useCounterStore = defineStore('counter', {
  state: () => ({ count: 0 }),
  getters: {
    double: (state) => state.count * 2,
  },
  actions: {
    increment() {
      this.count++
    },
  },
})

Setup Store

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

  return { count, doubleCount, increment }
})

推荐使用 Option Store,因为如果使用 Setup Store,会有局限性。比如:操作 state 的重置方法 $reset() 在 Setup Store 中就没有,需要自己实现。

[2] 使用 Store

拿到特定的 store

vue
<script setup>
  import { useCounterStore } from '@/stores/counter'
  const store = useCounterStore()
</script>

获取数据

vue
<script setup>
  // ❌ 这将不起作用,因为它破坏了响应性
  // 这就和直接解构 `props` 一样
  const { doubleCount } = store // doubleCount 将始终是 0
  setTimeout(() => {
    store.increment()
  }, 1000)
  // ✅ 这样写是响应式的
  const doubleValue = computed(() => store.doubleCount)
  const { doubleCount } = toRefs(store)
  const { doubleCount } = storeToRefs(store)
  // 作为 action 的 increment 可以直接解构
  const { increment } = store
</script>
2)操作 store

访问 state

通过 store 实例访问 state,直接对其进行读写。

javascript
const store = useStore()
store.count++

变更 state

允许用一个 state 的补丁对象在同一时间更改多个属性:

javascript
store.$patch({
  count: store.count + 1,
  age: 120,
  name: 'DIO',
})

重置 state

将 state 重置为初始值。

javascript
const store = useStore()
store.$reset()

替换 state

可以通过将其 $state 属性设置为新对象来替换 Store 的整个状态:

javascript
// 这实际上并没有替换`$state`
store.$state = { count: 24 }
3)Getter
[1] 访问当前 store 的 Getters
javascript
export const useCounterStore = defineStore('counter', {
  state: () => ({
    count: 0,
  }),
  getters: {
    doubleCount: (state) => state.count * 2,
  },
})

然后就可以直接访问 store 实例上的 getter 了:

vue
<script setup>
import { useCounterStore } from './counterStore'

const store = useCounterStore()
</script>

<template>
  <p>Double count is {{ store.doubleCount }}</p>
</template>
[2] Getters 中访问自己的其他 Getters
javascript
export const useCounterStore = defineStore('counter', {
  state: () => ({
    count: 0,
  }),
  getters: {
    doubleCount: (state) => state.count * 2,
    doubleCountPlusOne() {
      return this.doubleCount + 1
    },
  },
})
[3] 向 getter 传递参数

Getter 只是幕后的计算属性,所以不可以向它们传递任何参数。不过,可以从 getter 返回一个函数,该函数可以接受任意参数:

javascript
export const useUserListStore = defineStore('userList', {
  getters: {
    getUserById: (state) => {
      return (userId) => state.users.find((user) => user.id === userId)
    },
  },
})

并在组件中使用:

vue
<script setup>
  import { useUserListStore } from './store'
  const userList = useUserListStore()
  const { getUserById } = storeToRefs(userList) // 需要使用 getUserById.value 访问
</script>

<template>
  <p>User 2: {{ getUserById(2) }}</p>
</template>
[4] 访问其他 store 的 getter

想要使用另一个 store 的 getter 的话,那就直接在 getter 内使用就好:

javascript
import { useOtherStore } from './other-store'

export const useStore = defineStore('main', {
  state: () => ({
    // ...
  }),
  getters: {
    otherGetter(state) {
      const otherStore = useOtherStore()
      return state.localData + otherStore.data
    },
  },
})
4)Actions
[1] 定义

Action 相当于组件中的 method。它们可以通过 defineStore() 中的 actions 属性来定义,并且它们也是定义业务逻辑的完美选择。

javascript
export const useCounterStore = defineStore('main', {
  state: () => ({
    count: 0,
  }),
  actions: {
    increment() {
      this.count++
    },
    randomizeCounter() {
      this.count = Math.round(100 * Math.random())
    },
  },
})
[2] 使用

类似 getter,action 也可通过 this 访问整个 store 实例。

javascript
import { useAuthStore } from './auth-store'

export const useSettingsStore = defineStore('settings', {
  state: () => ({
    preferences: null,
    // ...
  }),
  actions: {
    async fetchUserPreferences() {
      const auth = useAuthStore()
      if (auth.isAuthenticated) {
        this.preferences = await fetchPreferences()
      } else {
        throw new Error('User must be authenticated')
      }
    },
  },
})

不同的是,action 可以是异步的。

javascript
import { mande } from 'mande'

const api = mande('/api/users')

export const useUsers = defineStore('users', {
  state: () => ({
    userData: null,
    // ...
  }),

  actions: {
    async registerUser(login, password) {
      try {
        this.userData = await api.post({ login, password })
        showTooltip(`Welcome back ${this.userData.name}!`)
      } catch (error) {
        showTooltip(error)
        // 让表单组件显示错误
        return error
      }
    },
  },
})

六、补充

1. 动态组件

见 Vue 3.x 文档。

2. 异步组件

在大型项目中,我们可能需要拆分应用为更小的块,并仅在需要时再从服务器加载相关组件。Vue 提供了 defineAsyncComponent 方法来实现此功能。

这个方法接受两种类型:

  • 类型一:工厂函数,该工厂函数需要返回一个 Promise 对象;
  • 类型二:接受一个对象类型,对异步函数进行配置;
1)基本使用
vue
<script setup>
import { defineAsyncComponent } from 'vue'

const AdminPage = defineAsyncComponent(() =>
  import('./components/AdminPageComponent.vue')
)
</script>

<template>
  <AdminPage />
</template>
2)加载与错误状态
javascript
const AsyncComp = defineAsyncComponent({
  // 加载函数
  loader: () => import('./Foo.vue'),

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

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

第三章:逻辑复用

一、mixin (混入)

1)功能:可以把多个组件共用的配置提取成一个混入对象。

2)使用方式

① 第一步:定义混合

在一个新的 js 文件中编写如下内容。

javascript
export const hunhe = {
  data(){....},
  methods:{....}
  // ......
}

② 第二步:使用混入

全局混入:Vue.mixin(xxx)

局部混入(Vue 的 vm 或 vc 配置对象中添加属性):mixins:[xxx]

注意:如果 mixin 数据与组件或者 Vue 实例冲突,优先以自己的为准;如果 mixin 定义的生命周期与组件或者 Vue 实例冲突,那么先执行 mixin 定义的生命周期,在执行自己的生命周期。

二、插件

1)功能:用于增强 Vue。

2)本质:包含 install 方法的一个对象。install 的第一个参数是 Vue,第二个及其以后的参数是插件使用者传递的数据。

3)如何使用?

① 定义插件

在一个新的 js 文件中编写如下内容。

javascript
// vm和vc都可以用
export default {
  install(Vue, ...args){
    // vue帮你调用install方法

    // 全局过滤器
    Vue.filter(...);

    // 全局指令
    Vue.directive(...);

    // 全局混入
    Vue.mixin(...);

    // 给vue原型上添加一个方法 vc/vm 都可以使用
    Vue.prototype.hello = function (){
      alert('hello')
    }
  }
}

② 在 @/main.js 导入

javascript
import plugins from './plugins'

③ 使用插件:Vue.use()

第四章:过渡 & 动画

一、transition 组件

官方文档:Transition

1. 是什么

如果希望给单元素或者组件实现过渡动画,可以使用 transition 内置组件来完成动画。Vue 提供了 transition 的封装组件,在下列情形中,可以给任何元素和组件添加进入 / 离开过渡:

  • 条件渲染 (v-if)、条件展示 (v-show)
  • 由特殊元素 <component> 切换的动态组件
  • 改变特殊的 key 属性

Transition 组件的原理

当插入或删除包含在 transition 组件中的元素时, Vue 将会做以下处理:

1)自动嗅探目标元素是否应用了 CSS 过渡或者动画 ,如果有,那么在恰当的时机添加 / 删除 CSS 类名。

2)如果 transition 组件提供了 JavaScript 钩子函数,这些钩子函数将在恰当的时机被调用。

3)如果没有找到 JavaScript 钩子并且也没有检测到 CSS 过渡或动画,那么 DOM 插入、删除操作将会立即执行。

总结:在插入、更新或移除 DOM 元素时,在合适的时候给元素添加样式类名。

2. CSS 过渡 class

Vue 2.x

  • 元素进入的样式

    v-enter:进入的起点。

    v-enter-active:进入过程中。

    v-enter-to:进入的终点。

  • 元素离开的样式

    v-leave:离开的起点。

    v-leave-active:离开过程中。

    v-leave-to:离开的终点。

Vue 3.x

  • v-enter-from:进入动画的起始状态。

    在元素插入之前添加,在元素插入完成后的下一帧移除。

  • v-enter-active:进入动画的生效状态。

    应用于整个进入动画阶段。在元素被插入之前添加,在过渡或动画完成之后移除。这个 class 可以被用来定义进入动画的持续时间、延迟与速度曲线类型。

  • v-enter-to:进入动画的结束状态。

    在元素插入完成后的下一帧被添加 (也就是 v-enter-from 被移除的同时) ,在过渡或动画完成之后移除。

  • v-leave-from:离开动画的起始状态。

    在离开过渡效果被触发时立即添加,在一帧后被移除。

  • v-leave-active:离开动画的生效状态。

    应用于整个离开动画阶段。在离开过渡效果被触发时立即添加,在过渡或动画完成之后移除。这个 class 可以被用来定义离开动画的持续时间、延迟与速度曲线类型。

  • v-leave-to:离开动画的结束状态。

    在一个离开动画被触发后的下一帧被添加 (也就是 v-leave-from 被移除的同时) ,在过渡或动画完成之后移除。

<Transition> 仅支持单个元素或组件作为其插槽内容。如果内容是一个组件,这个组件必须仅有一个根元素。

class 的 name 命名规则

如果我们使用的是一个没有 name 的 transition,那么所有的 class 是以 v- 作为默认前缀;如果我们添加了一个 name 属性,比如 <transition name="fade">,那么所有的 class 会以 fade- 开头。

vue
<Transition name="slide-fade">
  <p v-if="show">hello</p>
</Transition>

<style>
/*
  进入和离开动画可以使用不同
  持续时间和速度曲线。
*/
.slide-fade-enter-active {
  transition: all 0.3s ease-out;
}

.slide-fade-leave-active {
  transition: all 0.8s cubic-bezier(1, 0.5, 0.8, 1);
}

.slide-fade-enter-from,
.slide-fade-leave-to {
  transform: translateX(20px);
  opacity: 0;
}
</style>

3. CSS 的 animation

其他都与上面的一样,只有 *-enter-from 不是在元素插入后立即移除,而是在一个 animationend 事件触发时被移除。

与 CSS 过渡 class 一样,但大多数只会简单地使用 *-enter-active*-leave-active

vue
<Transition name="bounce">
  <p v-if="show" style="text-align: center;">
    Hello here is some bouncy text!
  </p>
</Transition>

<style>
.bounce-enter-active {
  animation: bounce-in 0.5s;
}
.bounce-leave-active {
  animation: bounce-in 0.5s reverse;
}
@keyframes bounce-in {
  0% {
    transform: scale(0);
  }
  50% {
    transform: scale(1.25);
  }
  100% {
    transform: scale(1);
  }
}
</style>

4. 常用属性

1)type

Vue 需要附加事件监听器,以便知道过渡何时结束。可以是 transitionend 或 animationend,这取决于你所应用的 CSS 规则。如果你仅仅使用二者的其中之一,Vue 可以自动探测到正确的类型。

然而在某些场景中,你或许想要在同一个元素上同时使用它们两个。举例来说,Vue 触发了一个 CSS 动画,同时鼠标悬停触发另一个 CSS 过渡。此时你需要显式地传入 type prop 来声明,告诉 Vue 需要关心哪种类型,传入的值是 animation 或 transition:

vue
<Transition type="animation">...</Transition>
2)duration

单位是毫秒值。

vue
<Transition :duration="1000">...</Transition>
3)mode (掌握)

除了通过 v-if / v-show 切换一个元素,也可以通过 v-if / v-else / v-else-if 在几个组件间进行切换,只要确保任一时刻只会有一个元素被渲染即可:

vue
<Transition>
  <button v-if="docState === 'saved'">Edit</button>
  <button v-else-if="docState === 'edited'">Save</button>
  <button v-else-if="docState === 'editing'">Cancel</button>
</Transition>

默认进入和离开的元素都是同时开始动画的,有什么方式可以先执行其中一个,在执行其中一个。

  • out-in:当前元素先进行过渡,完成之后新元素过渡进入。
  • in-out:新元素先进行过渡,完成之后当前元素过渡离开。
vue
<Transition mode="out-in">...</Transition>
4)appear

默认情况下,首次渲染的时候是没有动画的,如果我们希望给他添加上去动画,那么就可以增加另外一个属性 appear。

vue
<script setup>
import { shallowRef } from 'vue'
import CompA from './CompA.vue'
import CompB from './CompB.vue'

// use shallowRef to avoid component being deeply observed
const activeComponent = shallowRef(CompA)
</script>

<template>
	<label>
    <input type="radio" v-model="activeComponent" :value="CompA"> A
  </label>
  <label>
    <input type="radio" v-model="activeComponent" :value="CompB"> B
  </label>
  <Transition name="fade" mode="out-in" appear="true">
    <component :is="activeComponent"></component>
  </Transition>
</template>

<style>
.fade-enter-active,
.fade-leave-active {
  transition: opacity 0.5s ease;
}

.fade-enter-from,
.fade-leave-to {
  opacity: 0;
}
</style>

二、TransitionGroup

<TransitionGroup> 是一个内置组件,用于对 v-for 列表中的元素或组件的插入、移除和顺序改变添加动画效果。

<TransitionGroup> 支持和 <Transition> 基本相同的 props、CSS 过渡 class 和 JavaScript 钩子监听器,但有以下几点区别:

  • 默认情况下,它不会渲染一个容器元素。但你可以通过传入 tag prop 来指定一个元素作为容器元素来渲染。
  • 过渡模式在这里不可用,因为我们不再是在互斥的元素之间进行切换。
  • 列表中的每个元素都必须有一个独一无二的 key attribute。
  • CSS 过渡 class 会被应用在列表内的元素上,而不是容器元素上。
点我查看代码
vue
<script setup>
import { shuffle as _shuffle } from 'lodash-es'
import { ref } from 'vue'

const getInitialItems = () => [1, 2, 3, 4, 5]
const items = ref(getInitialItems())
let id = items.value.length + 1

function insert() {
  const i = Math.round(Math.random() * items.value.length)
  items.value.splice(i, 0, id++)
}

function reset() {
  items.value = getInitialItems()
  id = items.value.length + 1
}

function shuffle() {
  items.value = _shuffle(items.value)
}

function remove(item) {
  const i = items.value.indexOf(item)
  if (i > -1) {
    items.value.splice(i, 1)
  }
}
</script>

<template>
  <button @click="insert">Insert at random index</button>
  <button @click="reset">Reset</button>
  <button @click="shuffle">Shuffle</button>

  <TransitionGroup tag="ul" name="fade" class="container">
    <li v-for="item in items" class="item" :key="item">
      {{ item }}
      <button @click="remove(item)">x</button>
    </li>
  </TransitionGroup>
</template>

<style>
.container {
  position: relative;
  padding: 0;
  list-style-type: none;
}

.item {
  width: 100%;
  height: 30px;
  background-color: #f3f3f3;
  border: 1px solid #666;
  box-sizing: border-box;
}

/* 1. 声明过渡效果 */
.fade-move,
.fade-enter-active,
.fade-leave-active {
  transition: all 0.5s cubic-bezier(0.55, 0, 0.1, 1);
}

/* 2. 声明进入和离开的状态 */
.fade-enter-from,
.fade-leave-to {
  opacity: 0;
  transform: scaleY(0.01) translate(30px, 0);
}

/* 3. 确保离开的项目被移除出了布局流
      以便正确地计算移动时的动画效果。 */
.fade-leave-active {
  position: absolute;
}
</style>
preview
图片加载中
预览

Released under the MIT License.