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

二、脚手架文件结构

   ├── node_modules
   ├── public
   │   ├── favicon.ico: 页签图标
   │   └── index.html: 主页面
   ├── src
   │   ├── assets: 存放静态资源
   │   │   └── logo.png
   │   │── component: 存放组件
   │   │   └── HelloWorld.vue
   │   │── App.vue: 汇总所有组件
   │   │── main.js: 入口文件
   ├── .gitignore: git版本管制忽略的配置
   ├── 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 函数去指定具体内容。

四、vue.config.js 配置文件

1. 是什么

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

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

js
module.exports = {
  pages: {
    index: {
      // page 的入口
      entry: 'src/index/main.js',
      // 模板来源
      template: 'public/index.html',
      // 在 dist/index.html 的输出
      filename: 'index.html',
      // 当使用 title 选项时,
      // template 中的 title 标签需要是 <title><%= htmlWebpackPlugin.options.title %></title>
      title: 'Index Page',
      // 在这个页面中包含的块,默认情况下会包含
      // 提取出来的通用 chunk 和 vendor chunk
      // 决定了哪些 JavaScript chunk 会被包含到生成的 HTML 文件中
      chunks: ['chunk-vendors', 'chunk-common', 'index']
    },
    // 当使用只有入口的字符串格式时,
    // 模板会被推导为 `public/subpage.html`
    // 并且如果找不到的话,就回退到 `public/index.html`。
    // 输出文件名会被推导为 `subpage.html`。
    subpage: 'src/subpage/main.js'
  }
}

详情见:配置参考

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. ref 属性

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

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

3)使用方式

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

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

2. nextTick 方法

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

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

3)什么时候用:当改变数据后,要基于更新后的新 DOM 进行某些操作时,要在 nextTick 所指定的回调函数中执行。

二、组件间的通信

1. 父给子传

说明

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

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

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

3)接收数据

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

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

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

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

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

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

2. 子给父传

方法一:props + 回调

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

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

TodoList 案例学到的技巧

  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)绑定自定义事件

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

  2. 第二种方式:在父组件中使用的子组件标签上添加 ref 属性,这样在父组件 VC 的 refsVCmountedVCVCthis.emit('自定义事件名',数据)```。

    javascript
    <Demo ref="xxx"/>
    
    ......
    
    mounted(){
       this.$refs.xxx.$on('atXQ',this.test)
    }
  3. 若想让自定义事件只能触发一次,可以使用 once 修饰符,或 $once 方法。

  4. 触发自定义事件:this.$emit('atXQ',数据)

  5. 解绑自定义事件:this.$off('atXQ')this.$off(['atXQ','atCui'])this.$off() 解绑所有自定义事件。

  6. 组件上也可以绑定原生 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)取消订阅。

三、插槽 Slots

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

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

3)使用方式:

[1] 默认插槽

vue
父组件中:
        <Category>
           <div>html结构1</div>
        </Category>
子组件中:
        <template>
            <div>
               <!-- 定义插槽 -->
               <slot>插槽默认内容...</slot>
            </div>
        </template>

[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>
子组件中:
        <template>
            <div>
               <!-- 定义插槽 -->
               <slot name="center">插槽默认内容...</slot>
               <slot name="footer">插槽默认内容...</slot>
            </div>
        </template>

[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>

四、Vuex

1. 初识 Vuex

1)概念

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

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

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

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

3)工作原理

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

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。下面是在文件 src/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(对象写法)
     ...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(对象写法)
    ...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生成:incrementOdd、incrementWait(数组形式)
    ...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('countAbout',['sum','school','subject']),

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

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

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

javascript
// 方式一:自己直接dispatch
this.$store.dispatch('personAbout/addPersonWang',person)
// 方式二:借助mapActions
...mapActions('countAbout',{incrementOdd:'jiaOdd',incrementWait:'jiaWait'})

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

javascript
// 方式一:自己直接commit
this.$store.commit('personAbout/ADD_PERSON',person)
// 方式二:借助mapMutations
...mapMutations('countAbout',{increment:'JIA',decrement:'JIAN'}),

五、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
yarn add pinia
# 或者使用 npm
npm install pinia

2)集成 pinia

src/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
      }
    },
  },
})

第三章:逻辑复用

一、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')
        }
    }
}

② 在 src/main.js 导入

javascript
import plugins from './plugins'

③ 使用插件:Vue.use()

第四章:过渡 & 动画

一、transition 组件

1. 是什么

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

  • 条件渲染 (v-if)、条件展示 (v-show)
  • 动态组件
  • 组件根节点

Transition 组件的原理

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

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

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

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

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

2. 使用

1)准备好样式

  • 元素进入的样式:
    1. v-enter:进入的起点
    2. v-enter-active:进入过程中
    3. v-enter-to:进入的终点
  • 元素离开的样式:
    1. v-leave:离开的起点
    2. v-leave-active:离开过程中
    3. v-leave-to:离开的终点

2)使用 <transition> 包裹要过度的元素,并配置 name 属性。

vue
<transition name="hello">
   <h1 v-show="isShow">你好啊!</h1>
</transition>

备注:若有多个元素需要过度,则需要使用 <transition-group>,且每个元素都要指定 key 值。

第五章:路由

单页面应用程序(英文名:Single Page Application)简称 SPA,顾名思义,指的是一个 Web 网站中只有唯一的一个 HTML 页面,所有的功能与交互都在这唯一的一个页面内完成。这就需要用到路由。

一个路由(route)就是一组映射关系(key - value),多个路由需要路由器(router)进行管理。

前端路由:key 是路径,value 是组件。

一、基础

1. 快速入门

1)安装 vue-router

bash
npm i vue-router

# vue2 项目,要指定版本安装
npm i vue-router@3.5.2

说明

2022 / 07 以后,安装 vue-router 时,默认版本改为了 4,而 4 版本只能在 Vue3 中使用。只有 vue-router3 才能在 Vue2 使用。

2)引入与应用插件

src/main.js

javascript
// 1. 导入所需模块
import Vue from 'vue'
import VueRouter from 'vue-router'

// 2. 调用 Vue.use() 函数,将 VueRouter 安装为 Vue 的插件
Vue.use(VueRouter)

3)编写 router 配置项

新建 src/router/index.js 文件,编写路由配置。

javascript
// 引入VueRouter
import VueRouter from 'vue-router'
// 引入Luyou组件
import About from '../components/About'
import Home from '../components/Home'

// 配置路由规则
const routes = [
    {
        path:'/about',
        component:About
    },
    {
        path:'/home',
        component:Home
    }
]

// 创建router实例对象,去管理一组一组的路由规则
const router = new VueRouter({
   mode: 'history', // 或者是 hash
   routes,
})

// 暴露router
export default router

4)在 main.js 中创建 vm 时传入 router 配置项

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

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

5)实现切换(active-class 可配置高亮样式)

vue
<router-link active-class="active" to="/about">About</router-link>

6)指定展示位置

vue
<router-view></router-view>

任何子路由都是在其父路由的组件中切换显示,不管是多少层的路由嵌套。因此,在其父路由上需要写上 <router-view /> 承载容器来存放子路由组件。

几个注意点

① 路由组件通常存放在 pages|views 文件夹,一般组件通常存放在 components 文件夹。

② 通过切换,“隐藏”了的路由组件,默认是被销毁掉的,需要的时候再去挂载。

③ 每个组件都有自己的 $route 属性,里面存储着自己的路由信息。

④ 整个应用只有一个 router,可以通过组件的 $router 属性获取到。

2. 多级路由(多级路由)

1)配置路由规则,使用 children 配置项

javascript
routes:[
   {
      path:'/about',
      component:About,
   },
   {
      path:'/home',
      component:Home,
      children:[ // 通过children配置子级路由
         {
            path:'news', // 此处一定不要写:/news
            component:News
         },
         {
            path:'message',// 此处一定不要写:/message
            component:Message
         }
      ]
   }
]

2)跳转(要写完整路径)

vue
<router-link to="/home/news">News</router-link>

3. 命名路由

1)作用:可以简化路由的跳转。

2)如何使用

① 给路由命名

javascript
{
   path:'/demo',
   component:Demo,
   children:[
      {
         path:'test',
         component:Test,
         children:[
            {
               name:'hello' // 给路由命名
               path:'welcome',
               component:Hello,
            }
         ]
      }
   ]
}

② 简化跳转

vue
<!--简化前,需要写完整的路径 -->
<router-link to="/demo/test/welcome">跳转</router-link>

<!--简化后,直接通过名字跳转 -->
<router-link :to="{name:'hello'}">跳转</router-link>

<!--简化写法配合传递参数 -->
<router-link 
   :to="{
      name:'hello',
      query:{
         id:666,
         title:'你好'
      }
   }"
>跳转</router-link>

<router-link> 的 replace 属性

① 作用:控制路由跳转时操作浏览器历史记录的模式。

② 浏览器的历史记录有两种写入方式:分别为 pushreplacepush 是追加历史记录,replace 是替换当前记录。路由跳转时候默认为 push

③ 如何开启 replace 模式:<router-link replace .......>News</router-link>

4. 其他

1)路由重定向
javascript
{
    path: '/',
    redirect: '/discover/tuijian'
}
2)404 处理
javascript
// 方法一
{
    path: '*',
    component: () => import('@/views/NotFound.vue')
}

// 方法二
{
    name: 'NotFound',
    path: '/:pathMatch(.*)*',
    component: () => import('@/views/NotFound.vue')
}
3) 链接高亮(排它)

为什么超链接,不用 a 标签,而用 router-link 标签?

  • 使用 router-link 也会解析为 a 标签
  • 会给当前访问的超链接,自动加两个类名
    • router-link-exact-active --- 精确匹配类名
    • router-link-active --- 模糊匹配类名

如何更改默认给我们添加的类名?

javascript
const router = new VueRouter({
    routes: [...],
    linkActiveClass: "类名1",
    linkExactActiveClass: "类名2"
})

通过给上述两个类添加样式,实现导航链接的高亮效果。

二、进阶

1. 路由传参

1)query 参数

① 传递参数

vue
<!-- 跳转并携带query参数,to的字符串写法 -->
<router-link to="/home/message/detail?id=666&title=你好">跳转</router-link>
<router-link :to="`/home/message/detail?id=${id}&title=${title}`">跳转</router-link>          
<!-- 跳转并携带query参数,to的对象写法 -->
<router-link 
   :to="{
      path:'/home/message/detail',
      query:{
         id:666,
         title:'你好'
      }
   }"
>跳转</router-link>

② 接收参数

javascript
$route.query.id
$route.query.title
2)params 参数

① 配置路由,声明接收 params 参数

javascript
{
   path:'/home',
   component:Home,
   children:[
      {
         path:'news',
         component:News
      },
      {
         component:Message,
         children:[
            {
               name:'xiangqing',
               path:'detail/:id/:title', // 使用占位符声明接收params参数
               component:Detail
            }
         ]
      }
   ]
}

// 注: 在配置路由的时候,在占位的后面加上一个问号[params可以传递或者不传递]
// 比如: path: '/search/:keyword?',

② 传递参数

vue
<!-- 跳转并携带params参数,to的字符串写法 -->
<router-link :to="/home/message/detail/666/你好">跳转</router-link>
<router-link :to="`/home/message/detail/${m.id}/${m.title}`"></router-link>         
<!-- 跳转并携带params参数,to的对象写法 -->
<router-link 
   :to="{
      name:'xiangqing',
      params:{
         id:666,
         title:'你好'
      }
   }"
>跳转</router-link>

特别注意:路由携带 params 参数时,若使用 to 的对象写法,则不能使用 path 配置项,必须使用 name 配置!

③ 接收参数:

javascript
$route.params.id
$route.params.title
3)props 配置

作用:让路由组件更方便的收到参数。

javascript
{
   name:'xiangqing',
   path:'detail/:id',
   component:Detail,

   //第一种写法:props值为对象,该对象中所有的key-value的组合最终都会通过props传给Detail组件
   // props:{a:900}

   //第二种写法:props值为布尔值,布尔值为true,则把路由收到的所有params参数通过props传给Detail组件
   // props:true
   
   //第三种写法:props值为函数,该函数返回的对象中每一组key-value都会通过props传给Detail组件
   props(route){
      return {
         id:route.query.id,
         title:route.query.title
      }
   }
}
面试题
  1. 路由传递参数(对象写法)path 是否可以结合 params 参数一起使用?

    答: 路由跳转传参的时候,对象的写法可以是 name、path 形式,但是需要注意的是,path 这种写法不能与 params 参数一起使用。

  2. 如何指定 params 参数可传可不传?

javascript
如果路由要求传递params参数,但是你就不传递params参数,URL就会有问题
比如:配置路由的时候,占位了(params参数),但是路由跳转的时候就不传递
路径就会出现问题:
http://localhost:8080/#/?k=QWE
http://localhost:8080/#/search?k=QWE (正常情况下)

如何指定params参数可以传递、或者不传递,在配置路由的时候,在占位的后面加上一个问号[params可以传递或者不传递]
  1. params 参数可以传递也可以不传递,但是如果传递的是空串,如何解决?

    使用 undefined 解决 params 参数可以传递、不传递(空的字符串)。

    javascript
    this.$router.push({
        name:'search',
        params:{
            keyword:'' || undefined
        },
        query:{
            k:this.keyword.toUpperCase()
        }
    });
  2. 路由组件能不能传递 props 数据?

    可以总共有三种写法。

    javascript
    // 布尔值写法: params
    props: true,
    
    // 对象写法: 额外的给路由组件传递一些props
    props: { a: 1, b: 2 },
    
    // 函数写法: 可以params参数、query参数,通过props传递给路由组件
    props: ($route) => {
        return {
            keyword: $route.params.keyword,
            k: $route.query.k
        }
    }

2. 编程式路由导航

1)作用:不借助 <router-link> 实现路由跳转,让路由跳转更加灵活。

2)API

javascript
// *************************************************************** 字符串
this.$router.push('路由路径')
this.$router.replace('路由路径')

// *************************************************************** 对象
// ********************************************* path
// 写法一:对象格式,用 path 进行跳转
this.$router.push({
	  path: '完整路由地址',
      query: {
        参数: '值',
        参数: '值'    // query参数会拼接成  /xxxxx?参数=值&参数=值
      },
      // params: {
      //   参数: '值',
      //   参数: '值'
      // }, // 有问题,不用。参数不能拼接到url后面
})

// 写法二:对象格式,用 name 进行跳转
this.$router.push({
	  name: '路由的name值',
      query: {
        参数: '值',
        参数: '值'       // /xxxxx?参数=值&参数=值
      },
      params: {
        参数: '值',
        参数: '值'
      }, // 参数会拼到url后面    /xxxxx/3/绿皮书
})

// ********************************************* replace
this.$router.replace({
   name:'xiangqing',
   params:{
       id:xxx,
       title:xxx
   }
})
// *************************************************************** 前进后退
this.$router.forward() // 前进
this.$router.back() // 后退
this.$router.go() // 可前进也可后退 里面传递的是数字,正数前进几步,负数后退几步
跳转类型query 参数(id=1&name=xx)params 参数(/movie/1)
path×
name
完整字符串--

3. 缓存路由组件

1)作用:让不展示的路由组件保持挂载,不被销毁。

2)具体编码

vue
<keep-alive include="News"> 
    <router-view></router-view>
</keep-alive>

<keep-alive :include="['News','Message']"> 
    <router-view></router-view>
</keep-alive>

include 写的是组件名,不写 include 属性,将会缓存所有组件。

4. 两个新的生命周期钩子

1)作用:路由组件所独有的两个钩子,用于捕获路由组件的激活状态。

2)具体名字:

activated 路由组件被激活时触发。

deactivated 路由组件失活时触发。

5. 路由守卫

作用:对路由进行权限控制。

分类:全局守卫、独享守卫、组件内守卫

每个守卫方法接收三个参数:

  • to: Route:即将要进入的目标路由对象
  • from: Route:当前导航正要离开的路由。
  • next: Function:一定要调用该方法来 resolve 这个钩子。执行效果依赖 next 方法的调用参数。
    • next():进行管道中的下一个钩子。如果全部钩子执行完了,则导航的状态就是 confirmed (确认的)。
    • next(false):中断当前的导航。如果浏览器的 URL 改变了 (可能是用户手动或者浏览器后退按钮),那么 URL 地址会重置到 from 路由对应的地址。
    • next('/') 或者 next({ path: '/' }):跳转到一个不同的地址。当前的导航被中断,然后进行一个新的导航。你可以向 next 传递任意位置对象,且允许设置诸如 replace: truename: 'home' 之类的选项以及任何用在 router-linkto proprouter.push 中的选项。
    • next(error):(2.4.0+) 如果传入 next 的参数是一个 Error 实例,则导航会被终止且该错误会被传递给 router.onError() 注册过的回调。
1)全局守卫

src/router/index.js

javascript
// 全局前置守卫:初始化时执行、每次路由切换前执行
router.beforeEach((to,from,next)=>{
   console.log('beforeEach',to,from)
   if(to.meta.isAuth){ //判断当前路由是否需要进行权限控制
      if(localStorage.getItem('school') === 'hj44d45fh6*'){ //权限控制的具体规则
         next() //放行
      }else{
         alert('暂无权限查看')
         // next({name:'guanyu'})
      }
   }else{
      next() //放行
   }
})

// 全局后置守卫:初始化时执行、每次路由切换后执行
router.afterEach((to,from)=>{
   console.log('afterEach',to,from)
   if(to.meta.title){ 
      document.title = to.meta.title // 修改网页的title
   }else{
      document.title = 'vue_test'
   }
})
2)独享守卫

只作用于某个特定的路由,这些守卫定义在路由配置对象上。只有前置独享守卫,无后置独享守卫。

javascript
const router = new VueRouter({
    routes: [
        {
            path: '/foo',
            component: Foo,
            beforeEnter: (to, from, next) => {
              // ...
            }
        }
    ]
})

可以把全局守卫的前置守卫代码放到独享守卫里面,如下:

javascript
beforeEnter(to,from,next){
   console.log('beforeEnter',to,from)
   if(to.meta.isAuth){ // 判断当前路由是否需要进行权限控制
      if(localStorage.getItem('school') === 'hj44d45fh6*'){
         next()
      }else{
         alert('暂无权限查看')
         // next({name:'guanyu'})
      }
   }else{
      next()
   }
}
3)组件内守卫

只作用于某个特定的组件内部,这些守卫可以在组件内使用。

javascript
// 进入守卫:通过路由规则,进入该组件时被调用
beforeRouteEnter (to, from, next) {
},

// 离开守卫:通过路由规则,离开该组件时被调用
beforeRouteLeave (to, from, next) {
}

6. 路由器的两种工作模式

1)对于一个 url 来说,什么是 hash 值?—— # 及其后面的内容就是 hash 值。

2)hash 值不会包含在 HTTP 请求中,即:hash 值不会带给服务器。

3)hash 模式:

① 地址中永远带着 # 号,不美观。

② 若以后将地址通过第三方手机 app 分享,若 app 校验严格,则地址会被标记为不合法。

③ 兼容性较好。

4)history 模式:

① 地址干净,美观 。

② 兼容性和 hash 模式相比略差。

③ 应用部署上线时需要后端人员支持,解决刷新页面服务端 404 的问题。

注意:可以使用 connect-history-api-fallback 解决问题。

Released under the MIT License.