快速上手 vue.js <二>

续 vue <一>

快速上手 vue.js <二>_第1张图片

七、组件化-组件间通信

7.1 Vue组件的嵌套关系

  • 组件化的核心思想:

    • 对组件进行拆分,拆分成一个个小的组件
    • 再将这些组件嵌套再一起最终形成我们的应用程序
  • 最终整个程序会变成组件树

7.2 组件的通信方式

父子组件之间的通信:

父组件传递给子组件:通过 props 属性

子组件传递给父组件:通过 $emit 触发事件

7.2.1 父组件传递给子组件

  • 父组件传递给子组件:通过 props 属性

    • 让 App 决定子组件里面的数据
    • 父组件:App
    • 子组件:Props
  • Props

    • Props 是可以在组件上注册一些自定义的 attribute
    • 父组件给这些 attribute 赋值,子组件通过 attribute 的名称获取到对应的值
  • Props 有两种常见的用法

    • 方式一:字符串数组,数组中的字符串就是 attribute 的名称
    • 方式二:对象类型,对象类型可以在指定 attribute 名称的同时,指定它需要传递的类型、是否是必须的、默认值等等
      • 对传入的内容限制更多
        • 比如指定传入的 attribute 的类型 type

          • String  Number  Boolean  Array  Object  Date  Function  Symbol
        • 比如指定传入的 attribute 是否是必传的

        • 比如指定没有传入时,attribute 的默认值

  • Prop 的大小写命名(camelCase vs kebab-case)

    • HTML 中的 attribute 名是大小写不敏感的,所以浏览器会把所有大写字符解释为小写字符
    • 这意味着当使用 DOM 中的模板时,camelCase (驼峰命名法) 的 prop 名需要使用其等价的 kebab-case (短横线分隔命名) 命名
  • 父组件

<template>
  <div>
    <ShowInfo name="lili" age="33" height="1.88"></ShowInfo>
    <ShowInfo name="xiaoming" age="33" height="1.88"></ShowInfo>
    <ShowInfo name="lihaia" :age="33" height="1.88"></ShowInfo>
  </div>
</template>
<script>
import ShowInfo from "./ShowInfo.vue";
export default {
  components: {
    ShowInfo
  }
};
</script>
<style scoped></style>

  • 子组件
    • 数组语法缺点:不能对类型进行验证;没有默认值
    • 对象语法
      • : 转成数字类型
      • 默认值是对象必须写成函数形式,函数里面返回对象
      • 驼峰进行命名的话可以写 小驼峰 或者 短横线连接- 的形式
<template>
  <div>
    <!-- {{  }}:能够访问data 或者 props 中的数据 -->
    <h2>姓名:{{ name }}</h2>
    <h2>身高:{{ height }}</h2>
    <h2>年龄:{{ age }}</h2>
  </div>
</template>
<script>
export default {
  // 接收父组件传递过来的数据
     // 1. 数组语法
  props: ["name", "age", "height"],
    // 2. 对象语法
  props: {
    name: {
      type: String,
      default:"lili",
    },
    height:{
      type:Number,
      default:2
    },
    age: {
      type: Number,
      default: 44
      // 默认值和必传选一个
    },
        friend: {
      type: Object,
      default: () => ({
        name: "james",
      }),
    },
    // 也可以这么写
    hobbies: {
      type: Object,
      default() {
        return {
          name: "james",
        };
      },
    },
    hobbies: {
      type: Array,
      default: () => ["篮球", "数学"],
    },
  }
};
</script>
<style scoped></style>

7.2.2 非 props 的 attribute

  • 定义:传递给一个组件某个属性,但是该属性并没有定义对应的 props 或者 emits,包括 class 、style 、id 属性
  • 会默认添加到子组件的根元素上面去
  • 不希望添加到根元素上面,在组件中设置:inheritAttrs:false
    • 想要应用在根元素之外的其他元素,通过 a t t r s 进行访问: ‘ : c l a s s = " attrs进行访问:`:class=" attrs进行访问::class="attrs.address"`
  • 如果有多个根,必须显示地进行绑定:v-bind="$attrs.class"

7.2.3 子组件传递给父组件

  • 一般都是子组件中触发了某个事件,需要在父组件中进行响应

  • 或者子组件有一些内容想要传递给父组件

  • 步骤

    • 子组件
    <template>
      <div class="add">
        <button @click="btnClick(1)">+1</button>
        <button @click="btnClick(5)">+5</button>
        <button @click="btnClick(10)">+10</button>
        <button @click="btnClick(15)">+15</button>
        <button @click="btnClick(20)">+20</button>
      </div>
    </template>
    <script>
    export default {
        //告诉别人自己发出去了这个事件 
        //数组语法
      emits:["add"],
      methods: {
        btnClick(count) {
          console.log("btnClick", count);
        // 子组件发出去一个自定义事件
        // 第一个参数是自定义事件的名称
        // 第二个参数是传递的参数
        this.$emit('add',count)
      }
    }
    
    };
    </script>
    <style scoped>
    .add{
      display: flex;
    }
    
    button{
      margin: 20px 10px;
    }</style>
    
    
    • 父组件
    <template>
      <div class="app">
        <h1>当前计数:{{ counter }}</h1>
        <Add @add="addClick"></Add>
        <Sub @sub="subClick"></Sub>
      </div>
    </template>
    <script>
    import Add from "./Add.vue";
    import Sub from "./Sub.vue";
    export default {
      data() {
        return {
        counter:0
      }
    },
      components: {
        Add,
        Sub,
      },
      methods: {
        addClick(count) {
          this.counter+=count
        },
        subClick(count) {
          this.counter+=count
        }
      }
    };
    </script>
    <style scoped></style>
    
    
    • 子组件传递给父组件:通过 $emit 触发事件
      • 参数(自定义事件名称 ,参数 )
    • 父组件以@事件名进行绑定自身的方法,自身的方法修改数据
  • 自定义事件的参数和验证:声明自己发出去了事件

    • 数组语法
    • 对象语法:可进行验证(很少使用)

7.3 案例练习–tabControl

  • 父组件
<template>
  <div class="app">
    <!-- tab-control -->
    <TabControl :titles="['ddd', '1ddd', 'eee']" @tab-item-click="tabItemClick(index)"></TabControl>

    <!-- 展示内容 -->
    <h1>{{ pageContents[currentIndex] }}</h1>
  </div>
</template>
<script>
import TabControl from "./TabControl.vue";
export default {
  components: {
    TabControl,
  },
  data() {
    return {
      pageContents: ["衣服列表", "鞋子列表", "裤子列表"],
      currentIndex: 0,
    };
  },
  methods: {
    tabItemClick(index) {
      this.currentIndex = index;
    },
  },
};
</script>
<style scoped></style>

  • 子组件
<template>
  <div class="tab-control">
    <template v-for="(item, index) in titles" :key="item">
      <div :class="{active:currentIndex===index}" @click="itemClick(index)" class="item"><span>{{ item }}</span></div>
    </template>
  </div>
</template>
<script>
export default {
  emits:['tabItemClick'],
  props: {
    titles: {
      type: Array,
      default: () => [],
    },
  },
  data() {
    return {
      currentIndex: 0,
    };
  },
  methods: {
    itemClick(index) {
      this.currentIndex = index;
      this.$emit('tabItemClick',index)
    },
  },
};
</script>
<style scoped>
.tab-control {
  display: flex;
  height: 44px;
  line-height: 44px;
  text-align: center;
}
.item {
  flex: 1;
}
.active{
  color: brown;
  
  font-weight: 900;
}
.active span{
  color: brown;
}
</style>

八、Vue组件化 - 插槽Slot/非父子通信

8.1 插槽Slot

  • 在开发中,会经常封装一个个可复用的组件:

    • 通过 props 传递给组件一些数据,让组件来进行展示
    • 但是为了让这个组件具备更强的通用性,不能将组件中的内容限制为固定的 div、span 等等这些元素
    • 比如某种情况下使用组件,希望组件显示的是一个按钮,某种情况下使用组件希望显示的是一张图片
    • 应该让使用者可以决定某一块区域到底存放什么内容和元素
  • 插槽:让使用者决定里面使用什么,展示什么内容

  • 定义插槽 slot :

    • 插槽的使用过程其实是抽取共性、预留不同
    • 将共同的元素、内容依然在组件内进行封装
    • 会将不同的元素使用slot作为占位,让外部决定到底显示什么样的元素
    • 抽取共性、预留不同,将共同的元素内容依然在组件内进行封装,将不同的元素使用 slot 进行占位
    • 哈哈哈哈
  • 使用 slot

    • Vue 中将 元素作为承载分发内容的出口

    • 在封装组件中,使用特殊的元素就可以为封装组件开启一个插槽

    • 该插槽插入什么内容取决于父组件如何使用

    • <!-- 1. 内容是一个按钮 -->
      <ShowMessage title="zu件" message="chacao">
        <button>我是按钮元素!!!</button>
      </ShowMessage>
      <!-- 2. 内容是一个链接 -->
      <ShowMessage>
        <a href="http://baidu.com">我是超链接!!!</a>
      </ShowMessage>
        <!-- 2. 内容是一张图片 -->
        <ShowMessage>
      <img src="http://" alt="fg">
      </ShowMessage>
      <ShowMessage></ShowMessage>
      
  • 默认内容

8.2 具名插槽

  • 封装的组件
    • name:给插槽取名字,默认为 default
 <div class="nav-bar">
    <div class="left"><slot name="left">left</slot></div>
    <div class="center"><slot name="center">center</slot></div>
    <div class="right"><slot name="right">right</slot></div>
  </div>
  • 使用该组件
    • v-slot:缩写为 #
  <NavBar>
   <!-- 需要用template进行包裹 -->
   <template v-slot:left>
    <button>hh</button>
   </template>
   <template v-slot:center>
    <button>hh</button>
   </template>
   <template v-slot:right>
    <button>hh</button>
   </template>
  </NavBar>
  • 动态插槽名

    • 插槽的值不希望写死,动态的

    • <NavBar>
         <template v-slot:[position]>
           <span>哈哈哈哈</span>
         </template>
       </NavBar>
      

8.3 渲染作用域

  • 父级模板里面所有的内容都是在父级作用域中编译的
  • 子模板里面的所有内容都是在子作用域中编译的
  • 都有自己的作用域,不会跨域

8.4 作用域插槽

  • 封装的组件
  <div class="tab-control">
    <template v-for="(item, index) in titles" :key="item">
      <div :class="{ active: currentIndex === index }" @click="itemClick(index)" class="item">
      
        <!-- 把item传给slot -->
        <slot :item="item" :name="jja"
          ><span>{{ item }}</span></slot
        >
      </div>
    </template>
  </div>
  • 使用
    • 独占默认插槽的简写 v-slot
    • 只有一个插槽的时候可以省略 template,将 v-slot 设置在组件模板中
    • 混合使用时候不能省略 template
   <TabControl :titles="['ddd', '1ddd', 'eee']" @tab-item-click="tabItemClick">
      <template v-slot:default="props">
        <button>{{ props.item }}</button>
      </template>
    </TabControl>

8.5 Provide 和 Inject

  • provide/inject 用于非父子之间共享数据

    • 比如有一些深度嵌套的组件,子组件想要获取父组件的部分内

      在这种情况下,如果仍然将props沿着组件链逐级传递下

      去,就会非常的麻烦

  • 对于这种情况下,可以使用 Provide 和 Inject :

    • 无论层级结构有多深,父组件都可以作为其所有子组件的依赖

    提供者

    • 父组件有一个 provide 选项来提供数据;
      • 一般情况下写成函数
    • 子组件有一个 inject 选项来开始使用这些数据;
  • 应用于深度嵌套的组件,子组件需要使用父组件的一部分内容

  • 使用

    • 在父组件中添加属性provide{}

      • 动态数据:将provide设置为一个函数,return一个对象中使用this
      • message:computed(()=>{this.message})
    • 在子组件中添加属性inject:[]

8.6 全局事件总线 mitt库

  • vue2 实例本身就提供了事件总线的功能
  • vue3 把这个功能移除了,推荐 mitt 或 tiny-emitter

九、组件化–额外知识

9.1 组件的生命周期

  • 生命周期:创建–》挂载–》更新–》卸载

  • 组件的生命周期函数告诉我们目前组件正在哪一个过程

  • 生命周期函数

    • 生命周期函数是一些钩子函数(回调函数),在某个时间会被 Vue 源码内部进行回调
    • 回调函数,在某个事件会被vue源码内部进行回调
    • 通过回调,可以知道目前组件正在哪一个阶段
  • 生命周期的流程

    • beforecreate

      • 先创建对应的组件实例
    • created(发送网络请求、事件监听、this.$watch)

      • template 模板进行编译
    • beforeMount

      • 挂载到虚拟dom
    • mounted

      • 获取 dom
      • 根据虚拟 dom 生成真实 dom:用户可以看到
    • beforeUpdate

      • 数据更新
      • 根据最新数据生成新的 VNode
    • update

      • 不再使用 v-if=“false”
    • beforeunmount

      • 将之前挂载在虚拟 dom 中的 VNode 从虚拟 dom 中移除
    • unmounted(回收操作:取消事件监听)

      • 将组件实例销毁
export default {
  // 演示生命周期函数
  // 1. 组件被创建之前
  beforeCreate() {
    console.log(" beforeCreate");
  },
  // 2. 组件被创建完成
  created() {
    console.log("created");
    console.log("组件被创建完成");
    console.log("1. 发送网络请求,请求数据");
    console.log("2. 监听eventbus事件");
    console.log("3. 监听watch数据");
  },
  // 3. 组件template准备被挂载
  beforeMount() {
    console.log("beforemount");
    
  },
  // 4. 组件template被挂载:虚拟dom-》真实dom
  mounted() {
    console.log("mounted");
    console.log("获取dom");
    console.log("使用dom");
  },
  // 5. 数据发生改变
  // 5.1 准备更新dom
  beforeUpdate() {
    console.log("beforeUpdate");
  },
  // 5.2更新dom
  update() {
    console.log("update");
  },
  // 6. 准备卸载VNode->DOM元素
  // 6.1卸载之前
  beforeUnmount() {
    console.log("beforeUnmount");
  },
  // 6.2DOM元素被卸载完成
  unmounted() {
    console.log("卸载组件unmount");
  }
}

9.2 $ref 的使用

  • 想直接获取到元素对象或者子组件实例

    • 不要原生操作 dom ,不要主动获取 dom 并主动修改它的内容
    • 给元素或者组件绑定一个 ref 的 attribute 属性
  • 要获取元素

    • 定义:在元素中绑定 ref 并且赋值 ref=“btn”
    • 拿到元素:this.$refs.btn
    • 要获取组件:拿到的是组件实例 this.$refs.banner
      • 在父组件中可以主动地调用子组件的对象方法:this.$refs.banner.bannerClick()
      • 拿到组件的根元素:this. r e f s . b a n n e r . refs.banner. refs.banner.el
      • 对应的组件只有一个根元素,比较好维护
      • 组件实例还有两个属性:
        • $parent:访问父元素
        • $root:HelloWorld.vue 来实现
    <template>
      <div class="app">
        <h1>title:happiness is the most important thing!</h1>
        <h2 ref="message">{{ message }}</h2>
        <button ref="btn" @click="changeMessage">change message</button>
      </div>
      <Banner ref="banner"></Banner>
    </template>
    <script>
    import Banner from "./Banner.vue";
    export default {
      components: {
        Banner,
      },
      data() {
        return {
          message: "哈哈哈哈",
        };
      },
      methods: {
        changeMessage() {
          this.message = "hehehheehehehheh";
          // 1.获取元素
          console.log(this.$refs.message);
          console.log(this.$refs.btn);
          // 2. 获取组件实例
          console.log(this.$refs.banner);
          // 3. 在父组件中可以直接调用子组件的方法
          // this.$refs.banner.bannerClick()
          // 4. 获取子组件里面的元素:开发中推荐一个组件只有一个根
          console.log(this.$refs.banner.$el);
          // 5. 组件实例中有两个属性(少用)
          this.$parent//获取父组件
          this.$root//获取根组件
        },
      },
    };
    </script>
    <style scoped></style>
    
    

9.3 动态组件

  • 使用 component 组件,通过is属性来实现,大小写不敏感
    • is 的组件来源
      • 全局
      • 局部
  • 子组件
<template>
  <div class="home">
    <h1>home</h1>
    name:{{ name }}
    <button @click="homeBtnClick()">btn</button>
  </div>
</template>

<script>
export default {
  props: {
    name: {
      type: String,
      default:'hahh'
    }
  },
  methods: {
    homeBtnClick() {
      this.$emit('homeClick','home')
    }
  }
}

</script>
<style lang="less" scoped>

</style>
  • 父组件
<template>
  <div class="app">
    <h2>app</h2>
   <template v-for="(item,index) in tabs" :key="item">
    <button :class="{active:currentTab === item}" @click="itemClick(item)">{{ item }}</button>
  </template>
    <Component @homeBtnClick="handleClick" name='lili' :is="currentTab"></Component>
  </div>
</template>

<script>
import About from "./views/About.vue";
import Category from "./views/Category.vue";
import Home from './views/Home.vue';
export default {
  data() {
    return {
      tabs: ['home', 'about', 'category'],
      currentTab:'home'
    }
  },
  methods: {
    itemClick(item) {
      this.currentTab=item
    },
    handleClick(payload) {
      console.log(payload);
    }
  },
  components: {
    About,
    Category,
    Home,
  },
};
</script>
<style lang="less" scoped></style>

9.4 keep-alive 属性

  • 会将当前的组件缓存,保存里面的数据,不会将其销毁

  • include 属性:只缓存这两个属性,属性需要在组件定义时添加name属性

    • 数组、对象、正则
  • exclude 属性:排除

  • max属性:最多缓存几个

  • 对于 keep-alive 组件,监听有没有进行切换

    • activated
    • deactivated
<template>
  <div class="home">
    <h2>home</h2>
 
  </div>
</template>

<script>
export default {
  name: 'home',
  activated() {
    console.log("进入活跃状态!");
  },
  deactivated() {
    console.log("离开活跃状态~");
  }
}
</script>
<style lang="less" scoped>

</style>

9.5 异步组件的使用

  • 对源码 src 进行打包:npm run build
  • 对某一个组件单独打包
<script>
import { defineAsyncComponent } from "vue";
const AsyncAbout = defineAsyncComponent(()=>import('./views/About.vue'))
export default {
  data() {
    return {
      tabs: ["home", "about", "category"],
      currentTab: "home",
    };
  },
  components: {
    About:AsyncAbout,

  },
};
</script>
  • 一般使用路由懒加载

9.6 组件的 v-model

  • 子组件
<template>
  <div>counter:{{ modelValue }}</div>
  <button @click="changeCounter">changeCounter</button>
</template>
<script>
export default {
  props: {
    modelValue: {
      type: Number,
      default:8
    }
  },
  emits: ['update:modelValue'],
  methods: {
    changeCounter() {
      this.$emit("update:modelValue",10000)
    }
  }
}
</script>
<style scoped>
</style>
  • 父组件
<template>
  <div class="app">
    <!-- 1. input v-model -->
    <!-- <input type="text" v-model="message" /> -->
    <!-- 等价于 -->
    <!-- <input :value="message" @input="message = $event.target.value" /> -->
    <!-- 2. 组件的 model -->
    <!-- <Counter v-model="appCounter"></Counter> -->
    <!-- 等价于 -->
    <Counter :modelValue="appCounter" @update:modelValue="appCounter = $event"></Counter>
  </div>
</template>
<script>
import Counter from "./Counter.vue";
export default {
  data() {
    return {
      message: "hello",
      appCounter:100
    };
  },
  components: {
    Counter,
  },
};
</script>
<style scoped></style>

  • input 中:v-bind:value 的数据绑定和 @input 的事件监听
  • 将其 value attribute 绑定到一个名叫 modelValue 的 prop 上
  • 在其 input 事件被触发时,将新的值通过自定义的 update:modelValue 事件抛出
  • modelValue 可以自定义

9.7 组件的混入 Mixin

  • 组件和组件间可能会有相同的代码逻辑,希望对相同的代码逻辑进行抽取

  • 创建文件夹 mixins

  • export default {
      data() {
        return {
          message: "hello world",
        };
      },
    };
    
    
  • 组件中使用

  • export default {
      mixins: [messageMixin]
    }
    
  • 都返回 data 对象,会进行合并,属性名冲突保留组件自身数据

  • 生命周期会合并

  • 手动混入:在子组件中添加 mixins:[messageMixin]

  • 全局混入:app.mixin({方法})

  • vue3 中几乎不用

十、Vue3 – Composition API

10.1 option API 的弊端

  • 在Vue2中,编写组件的方式是 Options API
    • Options API 的一大特点就是在对应的属性中编写对应的功能模块;
    • 比如 data 定义数据、methods 中定义方法、computed 中定义计算属性、watch 中监听属性改变,也包括生命周期钩子;
  • 但是这种代码有一个很大的弊端:
    • 当实现某一个功能时,这个功能对应的代码逻辑会被拆分到各个属性中
    • 组件变得更大、更复杂时,逻辑关注点的列表就会增长,那么同一个功能的逻辑就会被拆分的很分散
    • 这种碎片化的代码使用理解和维护这个复杂的组件变得异常困难,并且隐藏了潜在的逻辑问题
    • 并且当处理单个逻辑关注点时,需要不断的跳到相应的代码块

10.2 Composition API好处(VCA)

  • 方便对代码做维护
  • 可以将对应的逻辑抽出去
    • 因为是普通的代码,封装到函数里面
    • 后面需要使用的话就十分方便
    • 函数式的写法更加灵活

10.3 Composition API

  • 编写代码的地方:setup 函数
  • setup其实就是组件的另外一个选项
    • 只不过这个选项强大到可以用它来替代之前所编写的大部分其他选项
    • 比如 methods、computed、watch、data、生命周期等等

10.4 setup 函数的参数

  • props
    • 父组件传递过来的属性
    • 对于定义 props 的类型,还是和之前的规则是一样的,在props选项中定义
    • 并且在 template 中依然是可以正常去使用 props 中的属性,比如 message
    • 如果在 setup 函数中想要使用 props,那么不可以通过 this 去获取
    • 因为 props 有直接作为参数传递到 setup 函数中,所以可以直接通过参数来使用即可
  • context
    • attrs:所有的非 prop 的 attribute
    • slots:父组件传递过来的插槽
    • emit:当组件内部需要发出事件时会用到emit(因为不能访问this,所以不可以通过 this.$emit 发出事件)

10.5 Reactive API

  • 使用 reactive 的函数 可以变成响应式
  • 这是因为当使用 reactive 函数处理数据之后,数据再次被使用时就会进行依赖收集
  • 当数据发生改变时,所有收集到的依赖都是进行对应的响应式操作(比如更新界面)
  • 事实上,编写的 data 选项,也是在内部交给了 reactive 函数将其编程响应式对象的
  • reactive API对传入的类型是有限制的,它要求必须传入的是一个对象或者数组类型:如果传入一个基本数据类型(String、Number、Boolean)会报一个警告
<template>
  <div class="home">
    <h2>home:{{ message }}</h2>
    <button @click="changeMessage">changeMessage</button>
    <h2>account:{{ account.username }}</h2>
    <h2>account:{{ account.password }}</h2>
    <button @click="changeAccount">changeAccount</button>

  </div>
</template>

<script>
import { reactive } from "vue";

export default {
  setup() {
    // 1. 定义普通的数据: 可以正常使用
    // 缺点:数据不是响应式
    const message = "hello lili";

    // 2. 定义响应式数据
    // 2.1 reactive函数:定义复杂类型的数据
    const account = reactive({
      username: "lili",
      password: 343434,
    });
    const changeMessage = () => {
      message = "lili nihao";
  
    };
    function changeAccount() {
      account.username='konbe'
    }
    return {
      message,
      account,
      changeMessage,
      changeAccount
    };
  },
};
</script>
<style lang="less" scoped></style>

  • isProxy:检查对象是否是由 reactive 或 readonly 创建的 proxy
  • isReactive
    • 检查对象是否是由 reactive创建的响应式代理:
    • 如果该代理是 readonly 建的,但包裹了由 reactive 创建的另一个代理,它也会返回 true
  • isReadonly:检查对象是否是由 readonly 创建的只读代理
  • toRaw
    • 返回 reactive 或 readonly 代理的原始对象(建议保留对原始对象的持久引用。请谨慎使用)
  • shallowReactive:创建一个响应式代理,它跟踪其自身 property 的响应性,但不执行嵌套对象的深层响应式转换 (深层还是原生对象)
  • shallowReadonly:创建一个 proxy,使其自身的 property 为只读,但不执行嵌套对象的深度只读转换(深层还是可读、可写的)

10.6 Ref API

  • 默认情况下在 template 中使用 ref 时候,vue 会自动对其进行解包(去除其中的 value)
    • 但是是浅层解包
      • 使用时候不需要 .value
  • ref 会返回一个可变的响应式对象,该对象作为一个 响应式的引用 维护着它内部的值
  • 在模板中引入ref的值时,Vue会自动进行解包操作,所以并不需要在模板中通过 ref.value 的方式来使用
  • 但是在 setup 函数内部,它依然是一个 ref 引用, 所以对其进行操作时,依然需要使用 ref.value 的方式
    // 2.2 ref函数 :定义简单类型的数据(也可以定义复杂类型的数据)
    // 返回的是 ref 对象
    const counter = ref(0)
    function increment() {
      counter.value++
    }
  • unref
    • 如果想要获取一个 ref 引用中的 value,那么也可以通过unref方法
    • 如果参数是一个 ref,则返回内部值,否则返回参数本身
    • 这是 val = isRef(val) ? val.value : val 的语法糖函数
  • isRef:判断值是否是一个 ref 对象
  • shallowRef:创建一个浅层的 ref 对象
  • triggerRef:手动触发和 shallowRef 相关联的副作用

10.7 reactive 和 ref 使用场景

  • reactive

    • 应用于本地的数据,本地产生的,不是服务器产生的
    • 多个数据之间是有关系/联系的,组织在一起会有特定的作用: eg:account password
     <form action="">
         账号:<input type="text" v-model="account.username" /> 
         密码:<input type="password" v-model="account.password" /></form>
    
  • ref

    • 其他的场景基本都用 ref
    • 定义本地的一些简单数据
    • 定义从网络中获取的数据(一般都是从服务器中获取)
      ⅰ. 一般的数据都是来自于服务器
      ⅱ. const musics = ref([])
      ⅲ. musics.value=serverMusics

10.8 readonly

  • 单向数据流

    • 子组件拿到数据后只能使用,不能修改
    • 如果确实要修改,那么应该将事件传递出去,由父组件来修改数据
  • react 的使用是非常灵活的,但有一个重要的原则:任何一个组件都应该像纯函数一样,不能修改传入的 props

  • 引入 readonly,会返回原始对象的只读代理(依然是一个 Proxy,这是一个 proxy 的 set 方法被劫持),强制函数规定子组件不能进行修改

  • 参数

    • 普通对象
    • reactive 返回的对象
    • ref 的对象
  • setup 中子组件传递给父组件数据,符合单项数据流

<template>
  <div>showinfo:{{ info }}</div>
  <button @click="showInfoBtn">showinfo</button>
</template>
<script>
export default {
  props: {
    info: {
      type: Object,
      default: () => ({}),
    },
  },
  emits: ["showInfoBtn"],
  setup(props, context) {
    function showInfoBtn() {
      context.emit("changeInfo", "koebb");
    }
    return {
      showInfoBtn,
    };
  },
};
</script>
<style scoped></style>
<template>
  <div>hah</div>
  <ShowInfo :info="info" @changeInfo="changeName"></ShowInfo>
</template>
<script>
import ShowInfo from './ShowInfo.vue';
import { reactive } from 'vue';

export default {
  components: {
    ShowInfo
  },
  setup() {
    // 本地定义多个数据,需要传递给子组件
    const info = reactive({
      name: "lili",
      age:22
    })
    function changeName(payload) {
      info.name=payload
    }
    return {
      info,changeName
    }
  }
}
</script>
<style scoped>
</style>
  • 创建出来的数据也是响应式的但是不允许修改: const roInfo = readonly(info)

10.9 toRefs

  • reactive解构后会变成普通的值,失去响应式
    • 为了使用方便
  • toRef 一次解构一个
<template>
  <div>hah</div>
  <h1>{{ name }}</h1>
  <h1>{{ age }}</h1>
<h2>{{ height }}</h2>
  <h3>{{ info }}</h3>
</template>
<script>
import { reactive, toRefs, toRef } from "vue";

export default {
  setup() {
    const info = reactive({
      name: "why",
      age: 22,
      height: 4.44,
    });
    const { name, age } = toRefs(info);
    const { height } = toRef(info, 'height');
    console.log(height);
    return {
      info,
      name,
      age,
      height,
    };
  },
};
</script>
<style scoped></style>

10.10 setup 不可以使用 this

  • 表达的含义是 this 并没有指向当前组件实例
  • 并且在 setup 被调用之前,data、computed、methods 等都没有被解析
  • 所以无法在setup中获取 this

10.11 computed

  • 当某些属性是依赖其他状态时,可以使用计算属性来处理

    • 在前面的 Options API 中,是使用 computed 选项来完成的
    • 在 Composition API 中,可以在 setup 函数中使用 computed 方法来编写一个计算属性
  • 直接在 setup 函数里面使用 computed 方法

<template>
  <div>hah</div>
  {{ fullName }}
  <button @click="setFullName">setFullName</button>
</template>
<script>
import { reactive } from "vue";
import { computed } from "vue";
export default {
  setup() {
    const names = reactive({
      firstName: "kobe",
      lastName: "ddd",
    });
    // 一般只使用getter
    // const fullName = computed(() =>{
    //   return names.firstName+ " "+ names.lastName
    // })

    // 如果 getter 和 setter都使用:传入一个对象
    const fullName = computed({
      set: function (newValue) {
        const tempNames = newValue.split(" ");
        names.firstName = tempNames[0];
        names.lastName = tempNames[1];
      },
      get: function () {
        return names.firstName + " " + names.lastName;
      },
    });
    function setFullName() {
      fullName.value = "lili hha";
    }
    return {
      fullName,
      setFullName,
    };
  },
};
</script>
<style scoped></style>

  • 返回的是 ref

10.12 Setup 中 ref 引入元素

  • 定义一个 ref 对象,绑定到元素或者组件的 ref 属性上
<template>
  <div>
    <h2 ref="titleRef">woshibiaoti</h2>
    <button ref="btnRef">woshianniu</button>
    <ShowInfo ref="showInfoRef"></ShowInfo>
  </div>
</template>
<script>
import { onMounted, ref } from 'vue';
import ShowInfo from '../03_setup其他函数/ShowInfo.vue';
export default {
  components: {
    ShowInfo
  },
  setup() {
    const titleRef = ref()
    const btnRef = ref()
    const showInfoRef = ref()
    onMounted(() => {
      console.log(titleRef.value);
      console.log(btnRef.value);
      // .value 才能拿到实例
      console.log(showInfoRef.value);
      showInfoRef.value.showInfoFoo()
    })
    return {
      titleRef,btnRef,showInfoRef
    }
  }
}
</script>
<style scoped>
</style>

10.13 生命周期钩子

  • setup 可以用来替代 data 、 methods 、 computed 等等这些选项,也可以替代 生命周期钩子
<template>
  <div>hah</div>
</template>
<script>
import { onMounted } from 'vue';

export default {
  setup() {
    // 是一个函数的调用,传入一个回调函数
    onMounted(() => {
      console.log("onMounted");
    })
  }
}
</script>
<style scoped>
</style>

10.14 Provide-Inject

  • 父组件
<template>
  <div>hah</div>
  <ShowInfo></ShowInfo>
</template>
<script>
import { provide } from 'vue';
import ShowInfo from './ShowInfo.vue'
export default {
  components:{ShowInfo},
  setup() {
    // 响应式数据
    const name = ref('lili')
    provide("name", name)
    provide("age",99)
  }

}
</script>
<style scoped>
</style>
  • 子组件
<template>
  <div>hah</div>
  <h1>{{ name }}</h1>
</template>
<script>
import { inject } from 'vue';

export default {
  setup() {
    const name = inject("name")
    return {
      name
    }
  }
}
</script>
<style scoped>
</style>

10.15 侦听数据的变化

  • 在前面的 Options API 中,可以通过 watch 选项来侦听 data 或者 props 的数据变化,当数据变化时执行某一些操作
  • 在Composition API 中,可以使用 watchEffect 和 watch 来完成响应式数据的侦听
    • watchEffect:用于自动收集响应式数据的依赖
    • watch:需要手动指定侦听的数据源
  • watch
<template>
  <div>hah</div>
  <h2>{{ message }}</h2>

  <button @click="message = 'hahhh'">btnchange</button>
  <button @click="info.name = 'konbe'">btnchange</button>
</template>
<script>
import { reactive, ref, watch } from "vue";
export default {
  setup() {
    // 1. 定义数据
    const message = ref("hello world");
    const info = reactive({
      name: "lili",
      age: 18,
    });
    // 2. 侦听数据的变化
    watch(message, (newValue, oldValue) => {
      console.log(newValue, oldValue);
    });
    // 同一个引用,同一个东西value
    // 默认对reactive对象有进行深度监听
    watch(
      info,
      (newValue, oldValue) => {
        console.log(newValue, oldValue);
        console.log(newValue === oldValue);
      },
      {
        immediate: true,
      }
    );


    // 3. 监听reactive 数据变化后,获取普通对象
    watch(
      () => ({ ...info }),
      (newValue, oldValue) => {
        console.log(newValue, oldValue);
      }, {
        immediate: true,
        deep:true
      }
    );
    return {
      message,
      info,
    };
  },
};
</script>
<style scoped></style>
  • watchEffect
<template>
  <div>hah</div>
  <h2>{{ counter }}</h2>
</template>
<script>
import { ref, watchEffect } from "vue";

export default {
  setup() {
    const counter = ref(0);
    const age = ref(33);
    // 传入的函数会被直接执行
    // 会自动收集依赖
    const stopWatch = watchEffect(() => {
      console.log(counter.value, age.value);

      // 希望停止监听
      if (counter.value >= 10) {
        stopWatch();
      }
    });
    return {
      counter,
      age,
    };
  },
};
</script>
<style scoped></style>

  • 直接传入回调函数,函数默认会直接被执行,不需要在前面绑定

  • 在执行的过程中,会自动收集依赖(依赖哪些响应式数据)

  • 没更新一次,会执行一次该函数

  • 与 watch 区别

    • watch必须指定数据源,watcheffect自动指定依赖项

      • watch 监听到改变的时候可以拿到改变前后的 value

      • watcheffect 默认直接执行一次,watch 需要设置 immediate 属性才能立即执行

10.16 hooks 练习

  • useCounter.js
import {ref} from'vue'
export default function useCounter() {
  const counter = ref(10)
  const increment=(()=>{
     counter.value++
   })
   const decrement=(()=>{
     counter.value--
  })
  return {
    increment,decrement,counter
  }
}
  • 组件
<template>
  <div>Homecounter:{{counter  }}</div>
  <button @click="increment">add</button>
  <button @click="decrement">sub</button>
</template>
<script>
import { ref } from 'vue';
import useCounter from '../hooks/useCounter'
export default {
  setup() {
  return {...useCounter()}
  }
}
</script>
<style scoped>
</style>
  • 修改标题useTitle.js
import { ref,watch } from "vue"
export default function changeTitle(titleValue) {
  // document.title = title
    //定义ref的引用数据
  const title = ref(titleValue)
  //监听title的改变
  watch(title,(newValue)=>{
      document.title = newValue
  },{
      immediate:true
  })
  //返回一个title,对应组件中修改value,在setup中调用一次
  return title
}

10.17 script setup语法

  • 在 script 里面加上 setup

  • 导入组件就可以直接使用,不需要注册

  • 顶层编写的代码直接暴露给 template

  • 定义 **props:**语法糖提供了一个定义在作用域中的函数 defineProps
    a. const props=defineProps({})

  • 定义 emits:defineEmits
    a. const emits=defineEmits([“showInfo”])

  • 暴露属性:defineExpose

  • 好处

    • 更少的样板内容,更简洁的代码
    • 能够使用纯 Typescript 声明 prop 和抛出事件
    • 更好的运行时性能
    • 更好的 IDE 类型推断性能

10.18 阶段案例

  • 模拟网络请求

    • 需要先定义:const highScore = ref({})
    • 拿到后端数据后:highScore.value = res.data
  • 组件化开发

  • 定义 props、动态传值

  • json 数据的引入

  • 样式:自己算宽度

  • computed 属性的应用,数组变成字符串==》join

十一、Vue-Router

11.1 前端路由

  • 路由其实是网络工程中的一个术语:
    • 在架构一个网络时,非常重要的两个设备就是路由器和交换机
    • 路由器给每台电脑分配 ip 地址
    • ip 地址映射到一台电脑设备
    • 事实上,路由器主要维护的是一个映射表(ip=>mac)
    • 映射表会决定数据的流向
  • 路由的概念在软件工程中出现,最早是在后端路由中实现的,原因是 web 的发展主要经历了这样一些阶段
    • 后端路由阶段
    • 前后端分离阶段
    • 单页面富应用(SPA)

11.2 后端路由

  • 后端路由:

    • 早期的网站开发整个 HTML 页面是由服务器来渲染

      • 服务器直接生产渲染好对应的 HTML 页面, 返回给客户端进行展示.
    • 一个页面有自己对应的网址, 也就是 URL,后端维护映射关系

    • URL 会发送到服务器, 服务器会通过正则对该URL进行匹配, 并且最后交给一个 Controller进行处理

    • Controller 进行各种处理, 最终生成 HTML 或者数据, 返回给前端.

    • 当页面中需要请求不同的路径内容时, 交给服务器来进行处理, 服务器渲染好整个页面, 并且将页面返回给客户端

    • 这种情况下渲染好的页面, 不需要单独加载任何的js和css, 可以直接交给浏览器展示, 这样也有利于 SEO 的优化

  • 后端路由的缺点

    • 一种情况是整个页面的模块由后端人员来编写和维护的
    • 另一种情况是前端开发人员如果要开发页面, 需要通过 PHPJava 等语言来编写页面代码
    • 而且通常情况下 HTML 代码和数据以及对应的逻辑会混在一起, 编写和维护都是非常糟糕的事情

11.3 前后端分离

  • 前端渲染的理解

    • 每次请求涉及到的静态资源都会从静态资源服务器获取,这些资源包括HTML+CSS+JS,然后在前端对这些请求回来的资源进行渲染

    • 需要注意的是,客户端的每一次请求,都会从静态资源服务器请求文件

    • 同时可以看到,和之前的后端路由不同,这时后端只是负责提供 API 了

  • 前后端分离阶段

    • 随着 Ajax 的出现, 有了前后端分离的开发模式
    • 后端只提供 AP I来返回数据,前端通过 Ajax 获取数据,并且可以通过 JavaScript 将数据渲染到页面中
    • 这样做最大的优点就是前后端责任的清晰,后端专注于数据上,前端专注于交互和可视化上
    • 并且当移动端(iOS/Android)出现后,后端不需要进行任何处理,依然使用之前的一套 API即可
    • 目前比较少的网站采用这种模式开发

11.4 SPA 页面

  • single page application
  • 修改页面的url地址,后面的 hash 会发生改变,但依然使用原来的页面
  • 让下面的内容渲染不同的组件
  • 页面不需要渲染整个页面
  • 好处
    • 只有第一次会加载页面, 以后的每次页面切换,只需要进行组件替换
    • 减少了请求体积,加快页面响应速度,降低了对服务器的压力
  • 坏处
    • 首页第一次加载时会把所有的组件以及组件相关的资源全都加载了
    • 导致首页加载时加载了许多首页用不上的资源
    • 造成网站首页打开速度变慢的问题,降低用户体验
  • 前端路由:服务器不需要维护这个映射关系,前端维护这个映射关系,根据不同的 url 渲染不同的组件,页面不进行整体的刷新

11.5 URL 的 hash

  • URL 的 hash也就是锚点**(#**), 本质上是改变 window.locationhref 属性

  • 可以通过直接赋值 location.hash 来改变 href, 但是页面不发生刷新

  • hash 的优势就是兼容性更好,在老版IE中都可以运行,但是缺陷是有一个 #,显得不像一个真实的路径

11.6 HTML5的History

  • replaceState:替换原来的路径
  • pushState:使用新的路径
  • popState:路径的回退
  • go:向前或向后改变路径
  • forward:向前改变路径
  • back:向后改变路径

11.7 vue-router

  • 目前前端流行的三大框架, 都有自己的路由实现:

    • Angular 的 ngRouter
    • React 的 ReactRouter
    • Vue 的 vue-router
  • Vue Router 是 Vue.js 的官方路由

    • 它与 Vue.js 核心深度集成,让用 Vue.js 构建单页应用(SPA)变得非常容易
    • 目前 Vue 路由最新的版本是4.x版本
  • vue-router 是基于路由和组件的

    • 路由用于设定访问路径URL, 将路径和组件映射起来
    • 在vue-router的单页面应用中, 页面的路径的改变就是组件的切换
  • 安装 Vue Router:npm install vue-router

  • 步骤

    • 创建 router,createWebHashHistory
      • 引入 createRouter
      • 配置映射关系
        • route
        • history
    • 创建组件
    • 导出 router
    • app 使用 router
    • 设置替换位置 router-view
    • 切换组件:router-link,需要加 to 属性
  • 使用

    • router=》index.js
    import { createRouter, createWebHashHistory,createWebHistory  } from 'vue-router'
    import Home from '../views/Home.vue'
    import About from '../views/About.vue'
    const router = createRouter({
      // 指定采用的模式:hash
      history: createWebHashHistory(),
        //history:createWebHistory(),
      // 映射关系
      routes: [
          {path:"/",redirect:"/home"},
          {path: "/home", component: Home },
          {path:"/about",component:About},
        {path:"/about",component:()=>import("../Views/Home.vue")}
        
     ]
    })
    export default router
    
    • app.vue
      • router-link
        • to
          • 对象
          • 路径名
        • replace
        • active-class
      • router-view
    <template>
    <div class="app">
      <h1>app content</h1>
      <div class="nav">
        <router-link to="/home" replace    active-class="active">首页</router-link>//replace不会记录历史,不能返回上一步
        <router-link to="/about">关于</router-link>
      </div>
      <router-view></router-view>
    </div>
    </template>
    <script setup>
    </script>
    <style scoped>
    /*   自定义属性名并修改 */
    .active{color:red}
    /*   使用系统默认,不需要添加*/
    .router-link-active{}
    </style>
    
    • main.js
    import { createApp } from 'vue'
    import App from './App.vue'
    import router from './router'
    const app = createApp(App)
    
    app.use(router)
    app.mount('#app')
    

11.8 路由懒加载

  • 写法:const Home = ()=>import(/*webpackChunkName:'home'*/"../views/Home.vue")
  • 可添加魔法注释:/*webpackChunkName:‘home’*/
    • 指定名字
  • 直接引入: {path:"/about",component:()=>import("../Views/Home.vue")}

11.9 路由的其他属性

  • name:独一无二的
    • 跳转的时候使用
    • 增加子路由
  • meta:自定义的数据
    • meta:{name:“lili”,age:99}

11.10 动态路由

  • { path: "/user/:id", component: () => import("../views/User.vue") }

  • 需要将给定匹配模式的路由映射到同一个组件

    • 可能有一个 User 组件,它应该对所有用户进行渲染,但是用户的ID是不同的;
    • 在 Vue Router 中,可以在路径中使用一个动态字段来实现,称之为 路径参数;
  • 用户界面拿到值

    • 在模板中:

      user{{ $route.params.id }}

    • 在代码中:

      • options api:this.$route.params.id

      • composition api :

        import { useRoute } from 'vue-router';
        const route = useRoute()
        console.log(route.params.id);
        
        • 每次都需要对应更新

          import { useRoute, onBeforeRouteUpdate } from "vue-router";
          
          // onBeforeRouteUpdate(() => {
          //   const route = useRoute();
          //   console.log(route.params.id);
          // });
          //第一次进入
          const route = useRoute();
          console.log(route.params.id);
          onBeforeRouteUpdate((to, from) => {
            console.log("from", from.params.id);
            console.log("to", to.params.id);
          });
          

11.11 not found

  • 对于哪些没有匹配到的路由,通常会匹配到固定的某个页面

  • 比如NotFound的错误页面中,可编写一个动态路由用于匹配所有的页面

  • { path: "/:pathMatch(.*)", component: () => import("../views/NotFound.vue") }

  • 可以通过 $route.params.pathMatch获取到传入的参数:

    not found:{{ $route.params.pathMatch }}

  • 对路径进行解析:path: “/:pathMatch(.*)*”

    • 解析成数组

11.12 代码的页面跳转

  • 点击按钮跳转
<template>
  <div class="app">
    <h1>app content</h1>
    <div class="nav">
      <router-link to="/home" replace active-class="active">首页</router-link>
      <router-link to="/about">关于</router-link>
      <router-link to="/user/999">用户</router-link>
      <router-link to="/user/559">用户</router-link>
    </div>
    <router-view></router-view>
  </div>

  <!-- 其他元素跳转 -->

  <button @click="btnClick">按钮</button>
</template>
<script setup>
import { useRouter } from "vue-router";
const router = useRouter();
const btnClick1 = () => {
  // 跳转到首页
  router.push("./home");
};
    
const btnClick = () => {
  // 跳转到首页
  router.push({
    // name: "./home",//没有重定向,不推荐
    path: "/home",
    // 对象形式还可以加参数,对应添加了字段
    query: {
      name: "lili",
      age: 88,
    },
  });
};
</script>
  • 点击按钮返回
import { useRouter } from "vue-router";
const router = useRouter();
const btnClick = () => {
  router.back(); //返回
  // router.forward(); //向前
  // router.go(19); //向前多少步
};

11.13 二级路由

  • name 必须的
  • 路由的嵌套
    • 目前匹配的 Home、About、User 等都属于第一层路由,在它们之间可以来回进行切换
    • Home 页面本身,也可能会在多个组件之间来回切换
      • 比如 Home 中包括 Product、Message,可以在 Home 内部来回切换
      • 使用嵌套路由,在 Home 中也使用 router-view 来占位之后需要渲染的组件
{
      path: "/home",
      component: Home,
      name: "home",
      children: [
        {
          path: "/home",
          redirect: "/home/recommend",
        },
        {
          path: "recommend",
          component: () => import("../views/HomeRecommend.vue"),
        },
      ],
    },

11.14 动态添加路由

  • 一开始某一些路由是没有注册的,但是登录了一个角色,这个角色具有某一些权限
  • router.addRoute
// 动态路由
let isAdmin = true;
if (isAdmin) {
  // 动态添加一级路由
  router.addRoute({
    path: "/admin",
    component: () => import("../views/Admin.vue"),
  });
  // 动态添加二级路由
  // 添加 vip 页面
  router.addRoute("home", {
    path: "vip",
    component: () => import("../views/HomeVip.vue"),
  });
}
  • 动态管理路由的其他方法:
    • 删除路由有以下三种方式:
      • 方式一:添加一个 name 相同的路由
      • 方式二:通过 removeRoute 方法,传入路由的名称
      • 方式三:通过 addRoute 方法的返回值回调
    • 路由的其他方法补充:
      • router.hasRoute():检查路由是否存在
      • router.getRoutes():获取一个包含所有路由记录的数组

11.15 路由导航守卫

  • 逻辑
    • 进入订单页面
      • 判断用户是否登陆
      • 根据逻辑进行不同的处理
        • 用户登陆成功
          • 直接进入到订单页面
        • 用户没有登陆
          • 直接进入登陆页面
            • 用户登陆成功,再次进入首页(订单)
// 路由导航守卫
// beforeEach:进行任何的路由跳转之前,传入的beforeEach中的函数都会被回调
router.beforeEach((to, from) => {
  console.log(to, from);
  if (to.path !== "/login") {
    return "/login";
  }
});
  • 逻辑==》js 代码

  • 路由导航守卫

    • 路由导航:/home - > /order
    • 守卫:中间跳转的环节进行拦截
  • vue-router 提供的导航守卫主要用来通过跳转或取消的方式守卫导航

  • 全局的前置守卫beforeEach是在导航触发时会被回调的:

    • 它有两个参数

      • to:即将进入的路由Route对象;
      • from:即将离开的路由Route对象;
    • 它有返回值

      • false:取消当前导航
      • 不返回或者undefined:进行默认导航
      • 返回一个路由地址
        • 可以是一个string类型的路径;
        • 可以是一个对象,对象中包含path、query、params等信息;
    • 可选的第三个参数:next(不推荐使用)

      • 在Vue2中我们是通过next函数来决定如何进行跳转的
      • 在Vue3中我们是通过返回值来控制的,不再推荐使用next函数,这是因为开发中很容易调用多次next
  • 需求

    • 进入订单页面(order),判断用户是否登陆(isLogin -> localStorage 保存 token)
    • 没有登陆,跳转到登陆页面,进行登陆操作
    • 登陆了,直接进入订单页面
router.beforeEach((to, from) => {
  const token = localStorage.getItem('token')
  if (to.path === '/order' && !token) {
    return '/login'
  }
});
router.beforeEach((to, from) => {
  const token = localStorage.getItem('token')
  if (to.path === '/order' && !token) {
    return '/login'
  }
});
<template>
  <div class="home">
    <h2>home</h2>
    <button @click="logoutLogin">退出登陆</button>
    <h1>query:{{ $route.query }}</h1>
    <router-view></router-view>
  </div>
</template>

<script setup>
function logoutLogin() {
  localStorage.removeItem('token')
}
</script>
<style lang="less" scoped>

</style>
  • 其他导航守卫
  • 完整的导航解析流程
    • 导航被触发
    • 在失活的组件里调用 beforeRouteLeave 守卫
    • 调用全局的 beforeEach 守卫
    • 重用的组件里调用 beforeRouteUpdate 守卫(2.2+)
    • 在路由配置里调用 beforeEnter
    • 解析异步路由组件
    • 在被激活的组件里调用 beforeRouteEnter
      • 不能通过 this 使用
      • 拿不到组件实例
    • 调用全局的 beforeResolve 守卫(2.5+):异步组件解析之后,在跳转之前
    • 导航被确认
    • 调用全局的 afterEach 钩子
    • 触发 DOM 更新
    • 调用 beforeRouteEnter 守卫中传给 next 的回调函数,创建好的组件实例会作为回调函数的参数传入

十二、高级语法补充

12.1 自定义指令

  • 在 Vue 的模板语法中有各种各样的指令:v-show、v-for、v-model 等等,除了使用这些指令之外,Vue 也允许来 自定义自己的指令

  • 注意:

    • 在 Vue 中,代码的复用和抽象主要还是通过组件
    • 通常在某些情况下,需要对 DOM 元素进行底层操作,这个时候就会用到自定义指令
  • 自定义指令分为两种

    • 自定义局部指令:组件中通过 directives 选项,只能在当前组件中使用
    • 自定义全局指令:app 的 directive 方法,可以在任意组件中被使用
  • 案例:当某个元素挂载完成后可以自定获取焦点

    • 实现方式一:定义 ref 绑定到 input 中,调用 focus
    import { onMounted, ref } from 'vue';
    export default function useInput() {
     
    const inputRef = ref()
    onMounted(() => {
      inputRef.value?.focus()
    })
    return {inputRef}
    }
    
    • 实现方式二:自定义一个 v-focus 的局部指令

      • 只需要在组件选项中使用 directives(options api)
      • 它是一个对象,在对象中编写自定义指令的名称(注意:这里不需要加v-)
      • 自定义指令有一个生命周期,是在组件挂载后调用的 mounted,可以在其中完成操作
      <script>
      export default {
        directives: {
          focus: {
            // 生命周期函数(自定义指令)
            mounted(el) {
              // console.log("v-focus 运用的元素被挂载了", el);
              el?.focus()
            },
          },
        },
      };
      </script>
      
      // 方式二:自定义指令(局部指令)
      const vFocus = {
        // 生命周期函数(自定义指令)
        mounted(el) {
          // console.log("v-focus 运用的元素被挂载了", el);
          el?.focus();
        },
      };
      
    • 实现方式三:自定义一个 v-focus 的全局指令

    export default function directiveFocus(app) {
      app.directive("focus", {
        mounted(el) {
          // console.log("v-focus 运用的元素被挂载了", el);
          el?.focus();
        },
      });
    }
    
    
    import directiveFocus from './focus'
    export default function useDirectives(app) {
      directiveFocus(app)
    }
    
    import { createApp } from "vue";
    import App from "./01_自定义指令/App.vue";
    import useDirectives from "./01_自定义指令/directives/index";
    const app = createApp(App);
    useDirectives(app);
    app.mount("#app");
    
    

12.2 指令的生命周期

  • created:在绑定元素的 attribute(class) 或事件监听器被应用之前调用
  • beforeMount:当指令第一次绑定到元素并且在挂载父组件之前调用
  • mounted:在绑定元素的父组件被挂载后调用
  • beforeUpdate:在更新包含组件的 VNode 之前调用
  • updated:在包含组件的 VNode 及其子组件的 VNode 更新后调用
  • beforeUnmount:在卸载绑定元素的父组件之前调用
  • unmounted:当指令与元素解除绑定且父组件已卸载时,只调用一次
<template>
  <div class="app">
    <button @click="counter++">+1</button>
    <button @click="showTitle = !showTitle">切换</button>
    <h2 v-if="showTitle" v-lili>当前计数:{{ counter }}</h2>
  </div>
</template>
<script setup>
import { ref } from "vue";

const counter = ref(0);
const showTitle = ref(true);
const vLili = {
  created() {
    console.log("created");
  },
  beforeMount() {
    console.log("beforeMount");
  },
  mounted() {
    console.log("mounted");
  },
  beforeUpdate() {
    console.log("beforeUpdate");
  },
  updated() {
    console.log("updated");
  },
  beforeUnmount() {
    console.log("beforeUnmount");
  },
  umounted() {
    console.log("umounted");
  },
};
</script>
<style scoped></style>

12.3 指令的参数和修饰符

  <input type="text">
    <!-- v-指令名称:参数(argument).修饰符="value" -->
    <Counter v-model:title.lazy="message"></Counter>
export default function directiveUnit(app) {
  app.directive("unit", {
    mounted(el, bindings) {
      const defaultText = el.textContent;
      let unit = bindings.value;
      if (!unit) {
        unit = "$";
      }
      el.textContent = unit + defaultText;
    },
  });
}

<template>
  <div class="app">

    <h2 v-lili>hhhhhh</h2>
    <!-- v-指令名称:参数(argument).修饰符="value" -->
    <h2 v-lili:name.abc.cba="message">hhhhhh</h2>
    <!-- 价格拼接单位符号 -->
    <h2 v-unit>{{ 111}}</h2>
  </div>
</template>
<script setup>
import { ref } from "vue";
const message = '你好呀李思达'

const vLili = {
  mounted(el,bindings) {
    console.log(el,bindings);
    el.textContent = bindings.value
  }
};
</script>
<style scoped></style>

12.4 时间格式化指令

  • 时间戳
    • 10 位:单位秒 * 1000
import dayjs from "dayjs";

export default function directiveFtime(app) {
  app.directive("ftime", {
    mounted(el, bindings) {
      // 1. 获取时间,并且转化成毫秒
      let timestamp = Number(el.textContent);;
      if (timestamp.length === 10) {
        timestamp *= 1000;
      }
      // 2. 获取传入的参数
      let value = bindings.value;
      if (!value) {
        value = "YYYY-MM-DD HH:mm:ss";
      }
      // 3. 对时间进行格式化
      const formatTime = dayjs(timestamp).format(value);
      el.textContent = formatTime;
    },
  });
}

<template>
  <div class="app">
    <h1 v-ftime="'YYYY-MM-DD'">{{ timestamp}}</h1>
  </div>
</template>
<script setup>
const timestamp = 1934458935
</script>
<style scoped>
</style>

12.5 Teleport

  • 在组件化开发中,封装一个组件A,在另外一个组件B中使用
    • 那么组件 A 中 template 的元素,会被挂载到组件 B 中 template 的某个位置
    • 最终应用程序会形成一颗DOM树结构
  • 但是某些情况下,希望组件不是挂载在这个组件树上的,可能是移动到Vue app之外的其他位置
    • 比如移动到 body元素上,或者其他的div#app之外的元素上
    • 可以通过teleport来完成
  • Teleport
    • 它是一个 Vue 提供的内置组件,类似于 react 的 Portals
    • teleport 翻译过来是心灵传输、远距离运输的意思
    • 它有两个属性
      • to:指定将其中的内容移动到的目标元素,可以使用选择器
      • disabled:是否禁用 teleport 的功能
<template>
  <div class="app">
    <div class="hello">
      <h1 class="content">
        <!-- <teleport to="body">
        <hello-world />
      </teleport> -->
        <teleport to="#abc">
          <hello-world />
        </teleport>
      </h1>
    </div>
    <div class="content">
      <teleport to="#abc">
        <h2>hhhhhh</h2>
      </teleport>
    </div>
  </div>
</template>

<script setup>
import HelloWorld from "./HelloWorld.vue";
</script>
<style scoped></style>

DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <link rel="icon" href="/favicon.ico">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Vite Apptitle>
    <style>
      #abc{
        background-color: antiquewhite;
      }
    style>
  head>
  <body>
    <div id="app">div>
    <div id="abc">div>
    <script type="module" src="/src/main.js">script>
  body>
html>

12.6 Suspense

  • 目前(2023-10-04)Suspense 显示的是一个实验性的特性,API 随时可能会修改
  • Suspense 是一个内置的全局组件,该组件有两个插槽
    • default:如果default可以显示,那么显示 default 的内容
    • fallback:如果default无法显示,那么会显示 fallback 插槽的内容
<template>
  <div class="app">
    <Suspense>
      <template #default>
        <async-home />
      </template>
      <template #fallback>
        <h2>loading</h2>
      </template>
    </Suspense>
  </div>
</template>

<script setup>
import { defineAsyncComponent } from "vue";
const AsyncHome = defineAsyncComponent(() => import("./AsyncHome.vue"));
</script>
<style scoped></style>

12.7 Vue 插件

  • 通常向 Vue 全局添加一些功能时,会采用插件的模式,它有两种编写方式
    • 对象类型:一个对象,但是必须包含一个 install 的函数,该函数会在安装插件时执行
    • 函数类型:一个 function,这个函数会在安装插件时自动执行
  • 插件可以完成的功能没有限制,比如下面的几种都是可以的
    • 添加全局方法或者 property,通过把它们添加到 config.globalProperties 上实现
    • 添加全局资源:指令/过滤器/过渡等
    • 通过全局 mixin 来添加一些组件选项
    • 一个库,提供自己的 API,同时提供上面提到的一个或多个功能
  • app.use 本质上是安装插件
    • 可以传入对象
    • 可以传入函数
// app.use(router)
// app.use(store)
// app.use(pinia)
import { createApp } from "vue";
import App from "./01_自定义指令/App.vue";
// import App from './02_内置组件补充/App.vue'
// import App from "./03_安装插件/App.vue";
import directives from "./01_自定义指令/directives/index";
// const app = createApp(App);
// directives(app)

// app.mount("#app");
createApp(App).use(directives).mount("#app");

// 安装插件
// 方式一:传入对象的情况
// app.use(router) 拿到对象去执行 install 函数
// app.use(store)
// app.use(pinia)
app.use({
  install: function (app) {
    console.log("传入对象的 install 被执行", app);
    // app.directive()
    // app.component()
  },
});
// function use(obj) {
//   obj.install(app)
// }
// use({
//   install:function(){}
// })

// 方式二:传入函数的情况
app.use(function (app) {
  console.log("传入函数的 install 被执行", app);
});
import directiveFocus from "./focus";
import directiveUnit from "./unit";
import directiveFtime from "./ftime";
// export default function useDirectives(app) {
//   directiveFocus(app)
//   directiveUnit(app)
//   directiveFtime(app)
// }
export default function directives(app) {
  directiveFocus(app);
  directiveUnit(app);
  directiveFtime(app);
}

  • router
    • 安装:npm i vue-router
    • 传入对象,执行 install,返回一个对象
    • 全局注册组件 router-link 、 router-view

12.8 h 函数

  • Vue 推荐在绝大数情况下使用模板来创建 HTML,一些特殊的场景需要 JavaScript 的完全编程的能力,这个时 候可以使用 渲染函数 ,它比模板更接近编译器
  • VNode 和 VDOM 的概念
    • Vue在生成真实的DOM之前,会将节点转换成VNode,而VNode组合在一起形成一颗树结构,就是虚拟DOM (VDOM)
    • 事实上,之前编写的 template 中的HTML 最终也是使用渲染函数生成对应的VNode
    • 如果想充分的利用JavaScript的编程能力,可以自己来编写 createVNode 函数,生成对应的VNode
  • 使用 h()函数
    • h() 函数是一个用于创建 vnode 的一个函数
    • 更准确的命名是 createVNode() 函数,但是为了简便,在 Vue 将之简化为 h() 函数

12.9 h 函数的使用

  • 接受三个参数

    • tag:标签名字
    • props:属性
    • []:子元素,内部放置的内容

    createVNode("div",{class:"abc"},[createVNode("h2",null,[]),createVNode("p",{},"我是内容哈哈哈哈")])

  • 注意事项

    • 如果没有 props,那么通常可以将 children 作为第二个参数传入
    • 如果会产生歧义,可以将null作为第二个参数传入,将 children 作为第三个参数传入
  • h 函数可以在两个地方使用

    • render 函数选项中
    • setup 函数选项中(setup本身需要是一个函数类型,函数再返回 h 函数创建的 VNode)
  • options api

<script>
import { h } from "vue";

export default {
  render() {
    return h("div", { class: "app" }, [h("h2", { class: "title" }, "我是标题"), h("p", { class: "content" }, "我是内容")]);
  },
};
</script>
<style scoped></style>

<script>
import { h } from "vue";
import Home from './Home.vue'
export default {
  data() {
    return {
      counter:0
  }
  },
  render() {
    return h("div", { class: "app" }, [
      h("h2",null,`当前计数:${this.counter}`),
      h("button",{onClick:this.increment},"+1"),
      h("button", { onClick: this.decrement }, "-1"),
      h(Home)
    ]);
  },
  methods: {
    increment() {
      this.counter++
    },
    decrement() {
      this.counter--
      
    }
  }
};
</script>
<style scoped></style>


  • setup
<template>
  <render />
</template>
<!-- <script>
import { h, ref } from "vue";
import Home from './Home.vue'
export default {
  setup() {

    const counter = ref(0)
    const increment = () => {
      counter.value++
    }
    const decrement = () => {
      counter.value--
    }
    return () => {
      h("div", { class: "app" }, [
      h("h2",null,`当前计数:${counter.value}`),
      h("button",{onClick:increment},"+1"),
      h("button", { onClick:decrement }, "-1"),
      h(Home)
    ])
    }
    
  }
};
</script> -->
<script setup>
import { h, ref } from "vue";
import Home from "./Home.vue";
const counter = ref(0);
const increment = () => {
  counter.value++;
};
const decrement = () => {
  counter.value--;
};
const render = () => {
  h("div", { class: "app" }, [h("h2", null, `当前计数:${counter.value}`), h("button", { onClick: increment }, "+1"), h("button", { onClick: decrement }, "-1"), h(Home)]);
};
</script>
<style scoped></style>

12.10 jsx 的 babel 配置

  • 在项目中使用 jsx,需要添加对 jsx 的支持
    • 通过 Babel 来进行转换(React 编写的 jsx 就是通过 babel 转换的)
    • 对于 Vue 来说,只需要在 Babel 中配置对应的插件即可
<script lang="jsx">
import About from './About.vue'
export default {
  data() {
    return {
      counter: 0,
    };
  },
  methods: {
    increment() {
      this.counter++;
    },
    decrement() {
      this.counter--;
    },
  },
  render() {
    return (
      <div class='app'>
        <h2>counter:{this.counter}</h2>
        <button onClick={(e) => this.decrement()}>-1</button>
        <button onClick={(e) => this.increment()}>+1</button>
        <About/>
      </div>
    );
  },
};
</script>
<style lang="less" scoped></style>

<!-- <script lang="jsx">
import { ref } from "vue";
import About from './About.vue'
export default {
  setup() {
    const counter = ref(0);
    const increment = () => {
      counter.value++;
    };
    const decrement = () => {
      counter.value--;
    };
    return () => (
      <div class='app'>
        <h2>counter:{counter.value}</h2>
        <button onClick={decrement}>-1</button>
        <button onClick={increment}>+1</button>
        <About />
      </div>
    );
  },
};
</script> -->
<template>
  <jsx />
</template>
<script setup lang="jsx">
import { ref } from "vue";
import About from "./About.vue";
const counter = ref(0);
const increment = () => {
  counter.value++;
};
const decrement = () => {
  counter.value--;
};
const jsx = () => (
  <div class='app'>
    <h2>counter:{counter.value}</h2>
    <button onClick={decrement}>-1</button>
    <button onClick={increment}>+1</button>
    <About />
  </div>
);
</script>
<style lang="less" scoped></style>

  • 安装 Babel 支持 Vue 的 jsx 插件:npm install @vue/babel-plugin-jsx -D
  • 在 babel.config.js 配置文件中配置插件:
  • 如果是 Vite 环境,需要安装插件: npm install @vitejs/plugin-vue-jsx -D
import { fileURLToPath, URL } from 'node:url'

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import jsx from '@vitejs/plugin-vue-jsx'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [
    vue(),
    jsx()
  ],
  resolve: {
    alias: {
      '@': fileURLToPath(new URL('./src', import.meta.url))
    }
  }
})

你可能感兴趣的:(vue.js,javascript,ecmascript,前端框架,前端,vue)