Web前端-Vue2+Vue3基础入门到实战项目-Day5(自定义指令, 插槽, 案例商品列表, 路由入门)

自定义指令

基本使用

  • 自定义指令: 自己定义的指令, 可以封装一些dom操作, 扩展额外功能

  • 全局注册

    js 复制代码
    // 1. 全局注册指令
    Vue.directive('focus', {
      // inserted 会在 指令所在的元素, 被插入到页面中时触发
      inserted (el) {
          // el 就是指令所绑定的元素
          // console.log(el)
          el.focus()
      }
    })
  • 局部注册

    html 复制代码
    <script>
    export default {
    // mounted(){
    //   this.$refs.inp.focus()
    // }
    
    // 2. 局部注册指令
    directives: {
        // 指令名: 指令的配置项
        focus: {
        inserted (el) {
            el.focus()
        }
        }
    }
    }
    </script>

指令的值

  • v-指令名="指令值", 通过等号可以绑定指令的值
  • 通过binding.value可以拿到指令的值
  • 通过update钩子, 可以监听指令值的变化, 进行dom更新操作
html 复制代码
<template>
  <div>
    <h1 v-color="color1">指令的值1测试</h1>
    <h1 v-color="color2">指令的值2测试</h1>
  </div>
</template>

<script>
export default {
  data () {
    return {
      color1: 'red',
      color2: 'green'
    }
  },
  directives: {
    color: {
      // 1. inserted 提供的是元素被添加到页面中时的逻辑
      inserted (el, binding) {
        // binding.value 就是指令的值
        el.style.color = binding.value
      },
      // 2. update 指令的值修改的时候触发, 提供值变化后, dom更新的逻辑
      update(el, binding){
        // console.log(binding.value)
        el.style.color = binding.value
      }
    }
  }
}
</script>

<style>

</style>

封装v-loading指令

  • 核心思路
    1. 准备类名loading, 通过伪元素提供遮罩层
    2. 添加或移除类名, 实现loading蒙层的添加移除
    3. 利用指令语法, 封装v-loading通用指令
      inserted钩子中, binding.value判断指令的值, 设置默认状态
      update钩子中, binding.value判断指令的值, 更新类名状态
html 复制代码
<template>
  <div class="main">
    <div class="box" v-loading="isLoading">
      <ul>
        <li v-for="item in list" :key="item.id" class="news">
          <div class="left">
            <div class="title">{{ item.title }}</div>
            <div class="info">
              <span>{{ item.source }}</span>
              <span>{{ item.time }}</span>
            </div>
          </div>

          <div class="right">
            <img :src="item.img" alt="">
          </div>
        </li>
      </ul>
    </div>
  </div>
</template>

<script>
// 安装axios =>  yarn add axios
import axios from 'axios'

// 接口地址:http://hmajax.itheima.net/api/news
// 请求方式:get
export default {
  data () {
    return {
      list: [],
      isLoading: true
    }
  },
  directives: {
    loading(el, binding){
      binding.value ? el.classList.add('loading') : el.classList.remove('loading')
    },
    update(el, binding){
      binding.value ? el.classList.add('loading') : el.classList.remove('loading')
    }
  },
  async created () {
    // 1. 发送请求获取数据
    const res = await axios.get('http://hmajax.itheima.net/api/news')
    
    setTimeout(() => {
      // 2. 更新到 list 中
      this.list = res.data.data
      this.isLoading = false
    }, 2000)
  }
}
</script>

<style>
/* 伪类 - 蒙层效果 */
.loading:before {
  content: '';
  position: absolute;
  left: 0;
  top: 0;
  width: 100%;
  height: 100%;
  background: #fff url('./loading.gif') no-repeat center;
}

/* .box2 {
  width: 400px;
  height: 400px;
  border: 2px solid #000;
  position: relative;
} */

.box {
  width: 800px;
  min-height: 500px;
  border: 3px solid orange;
  border-radius: 5px;
  position: relative;
}
.news {
  display: flex;
  height: 120px;
  width: 600px;
  margin: 0 auto;
  padding: 20px 0;
  cursor: pointer;
}
.news .left {
  flex: 1;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  padding-right: 10px;
}
.news .left .title {
  font-size: 20px;
}
.news .left .info {
  color: #999999;
}
.news .left .info span {
  margin-right: 20px;
}
.news .right {
  width: 160px;
  height: 120px;
}
.news .right img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}
</style>

插槽

默认插槽

  • 使用步骤

    1. 先在组件内用slot占位
    2. 使用组件时, 传入具体标签内容插入
  • 插槽中的内容会作为默认值

  • MyDialog.vue

    html 复制代码
    <template>
      <div class="dialog">
        <div class="dialog-header">
          <h3>友情提示</h3>
          <span class="close">✖️</span>
        </div>
    
        <div class="dialog-content">
          <!-- 1. 在需要定制的位置, 使用slot占位 -->
          <slot></slot>
        </div>
        <div class="dialog-footer">
          <button>取消</button>
          <button>确认</button>
        </div>
      </div>
    </template>
    
    <script>
    export default {
      data () {
        return {
    
        }
      }
    }
    </script>
    
    <style scoped>
    * {
      margin: 0;
      padding: 0;
    }
    .dialog {
      width: 470px;
      height: 230px;
      padding: 0 25px;
      background-color: #ffffff;
      margin: 40px auto;
      border-radius: 5px;
    }
    .dialog-header {
      height: 70px;
      line-height: 70px;
      font-size: 20px;
      border-bottom: 1px solid #ccc;
      position: relative;
    }
    .dialog-header .close {
      position: absolute;
      right: 0px;
      top: 0px;
      cursor: pointer;
    }
    .dialog-content {
      height: 80px;
      font-size: 18px;
      padding: 15px 0;
    }
    .dialog-footer {
      display: flex;
      justify-content: flex-end;
    }
    .dialog-footer button {
      width: 65px;
      height: 35px;
      background-color: #ffffff;
      border: 1px solid #e1e3e9;
      cursor: pointer;
      outline: none;
      margin-left: 10px;
      border-radius: 3px;
    }
    .dialog-footer button:last-child {
      background-color: #007acc;
      color: #fff;
    }
    </style>
  • App.vue

    html 复制代码
    <template>
      <div>
        <!-- 2. 在使用组件时, 组件标签填入内容 -->
        <MyDialog>你确认要删除吗</MyDialog>
    
        <MyDialog>你确认要退出吗</MyDialog>
      </div>
    </template>
    
    <script>
    import MyDialog from "./components/MyDialog.vue"
    export default {
      data() {
        return {}
      },
      components: {
        MyDialog,
      },
    }
    </script>
    
    <style>
    body {
      background-color: #b3b3b3;
    }
    </style>

具名插槽

  • slot占位, 给name属性起名字来区分
  • template配合v-slot:插槽名分发内容
  • v-slot:插槽名 可以简化为 #插槽名

App.vue

html 复制代码
<template>
  <div>
    <MyDialog>
      <!-- 需要通过template标签包裹分发的结构 -->
      <template v-slot:head>
        <div>我是大标题</div>
      </template>
      <template v-slot:content>
        <div>我是内容</div>
      </template>
      <template #footer>
        <button>确认</button>
        <button>取消</button>
      </template>
    </MyDialog>
  </div>
</template>

<script>
import MyDialog from './components/MyDialog.vue'
export default {
  data () {
    return {

    }
  },
  components: {
    MyDialog
  }
}
</script>

<style>
body {
  background-color: #b3b3b3;
}
</style>

MyDialog.vue

html 复制代码
<template>
  <div class="dialog">
    <div class="dialog-header">
      <!-- 一旦插槽起了名字, 就是具名插槽, 就只支持定向分发 -->
      <slot name="head"></slot>
    </div>

    <div class="dialog-content">
      <slot name="content"></slot>
    </div>
    <div class="dialog-footer">
      <slot name="footer"></slot>
    </div>
  </div>
</template>

<script>
export default {
  data() {
    return {}
  },
}
</script>

<style scoped>
* {
  margin: 0;
  padding: 0;
}
.dialog {
  width: 470px;
  height: 230px;
  padding: 0 25px;
  background-color: #ffffff;
  margin: 40px auto;
  border-radius: 5px;
}
.dialog-header {
  height: 70px;
  line-height: 70px;
  font-size: 20px;
  border-bottom: 1px solid #ccc;
  position: relative;
}
.dialog-header .close {
  position: absolute;
  right: 0px;
  top: 0px;
  cursor: pointer;
}
.dialog-content {
  height: 80px;
  font-size: 18px;
  padding: 15px 0;
}
.dialog-footer {
  display: flex;
  justify-content: flex-end;
}
.dialog-footer button {
  width: 65px;
  height: 35px;
  background-color: #ffffff;
  border: 1px solid #e1e3e9;
  cursor: pointer;
  outline: none;
  margin-left: 10px;
  border-radius: 3px;
}
.dialog-footer button:last-child {
  background-color: #007acc;
  color: #fff;
}
</style>

作用域插槽

  • 作用: 可以给插槽上绑定数据, 供将来使用组件时使用

  • 使用步骤:

    1. 给slot标签, 以添加属性的方式传值
    2. 所有属性都会被收集到一个对象中
    3. template中, 通过#插槽名="obj"接收
  • App.vue

    html 复制代码
    <template>
      <div>
        <MyTable :data="list">
          <!-- 3. 通过template #插槽名="变量名" 接收 -->
          <template #default="obj">
            <button @click="del(obj.row.id)">
              删除
            </button>
          </template>
        </MyTable>
        <MyTable :data="list2">
          <template #default="{ row }">
            <button @click="show(row)">
              查看
            </button>
          </template>
        </MyTable>
      </div>
    </template>
    
    <script>
    import MyTable from './components/MyTable.vue'
    export default {
      data () {
        return {
          list: [
            { id: 1, name: '张小花', age: 18 },
            { id: 2, name: '孙大明', age: 19 },
            { id: 3, name: '刘德忠', age: 17 },
          ],
          list2: [
            { id: 1, name: '赵小云', age: 18 },
            { id: 2, name: '刘蓓蓓', age: 19 },
            { id: 3, name: '姜肖泰', age: 17 },
          ]
        }
      },
      components: {
        MyTable
      },
      methods: {
        del(id){
          this.list = this.list.filter(item => item.id !== id)
        },
        show(row){
          // console.log(row)
          alert(`姓名: ${row.name}\n年纪: ${row.age}`)
        }
      }
    }
    </script>
  • MyTable.vue

    html 复制代码
    <template>
      <table class="my-table">
        <thead>
          <tr>
            <th>序号</th>
            <th>姓名</th>
            <th>年纪</th>
            <th>操作</th>
          </tr>
        </thead>
        <tbody>
          <tr v-for="(item, index) in data" :key="item.id">
            <td> {{index+1}} </td>
            <td> {{item.name}} </td>
            <td> {{item.age}} </td>
            <td>
              <!-- 1. 给slot标签, 添加属性的方式传值 -->
              <slot :row="item" msg="test"></slot>
              <!-- 2. 将所有的属性, 添加到一个对象中 -->
              <!-- 
                {
                  row: { id: 2, name: '孙大明', age: 19 },
                  msg: `test`
                }
              -->
            </td>
          </tr>
        </tbody>
      </table>
    </template>
    
    <script>
    export default {
      props: {
        data: Array,
      },
    }
    </script>
    
    <style scoped>
    .my-table {
      width: 450px;
      text-align: center;
      border: 1px solid #ccc;
      font-size: 24px;
      margin: 30px auto;
    }
    .my-table thead {
      background-color: #1f74ff;
      color: #fff;
    }
    .my-table thead th {
      font-weight: normal;
    }
    .my-table thead tr {
      line-height: 40px;
    }
    .my-table th,
    .my-table td {
      border-bottom: 1px solid #ccc;
      border-right: 1px solid #ccc;
    }
    .my-table td:last-child {
      border-right: none;
    }
    .my-table tr:last-child td {
      border-bottom: none;
    }
    .my-table button {
      width: 65px;
      height: 35px;
      font-size: 18px;
      border: 1px solid #ccc;
      outline: none;
      border-radius: 3px;
      cursor: pointer;
      background-color: #ffffff;
      margin-left: 5px;
    }
    </style>

案例 - 商品列表

my-tag 标签组件的封装
1. 创建组件 - 初始化
2. 实现功能
  2.1 双击显示, 并且自动聚焦
    v-if v-else @dblclick
    自动聚焦
    1. $nextTick => $refs 获取到dom, 进行focus获取焦点
    2. 封装v-focus指令
  2.2 失去焦点, 隐藏输入框
    @blur 操作 isEdit
  2.3 回显标签内容
    回显的标签信息是父组件传递过来的
    v-model实现功能(简化代码) v-model => :value 和 @input
    组件内部通过props接收, :value设置给输入框
  2.4 内容修改, 回车=> 修改标签信息
    @keyup.enter, 触发事件 $emit('input', e.target.value)
------------------------
my-table 表格组件的封装
1. 数据不能写死, 动态传递表格渲染的数据 props
2. 结构不能写死 - 多处结构自定义 [具名插槽]
  2.1 表头支持自定义
  2.2 主体支持自定义
  • App.vue

    html 复制代码
    <template>
      <div class="table-case">
        <MyTable :data="goods">
          <template #head>
            <th>编号</th>
            <th>名称</th>
            <th>图片</th>
            <th width="100px">标签</th>
          </template>
          <template #body="{item, index}">
            <td> {{index+1}} </td>
            <td> {{item.name}} </td>
            <td>
              <img :src="item.picture" />
            </td>
            <td>
              <!-- 标签组件 -->
              <MyTag v-model="item.tag"></MyTag>
            </td>
          </template>
        </MyTable>
      </div>
    </template>
    
    <script>
    import MyTag from './components/MyTag.vue'
    import MyTable from './components/MyTable.vue'
    export default {
      name: 'TableCase',
      components: {
        MyTag,
        MyTable
      },
      methods: {
    
      },
      data() {
        return {
          goods: [
            {
              id: 101,
              picture:
                'https://yanxuan-item.nosdn.127.net/f8c37ffa41ab1eb84bff499e1f6acfc7.jpg',
              name: '梨皮朱泥三绝清代小品壶经典款紫砂壶',
              tag: '茶具',
            },
            {
              id: 102,
              picture:
                'https://yanxuan-item.nosdn.127.net/221317c85274a188174352474b859d7b.jpg',
              name: '全防水HABU旋钮牛皮户外徒步鞋山宁泰抗菌',
              tag: '男鞋',
            },
            {
              id: 103,
              picture:
                'https://yanxuan-item.nosdn.127.net/cd4b840751ef4f7505c85004f0bebcb5.png',
              name: '毛茸茸小熊出没,儿童羊羔绒背心73-90cm',
              tag: '儿童服饰',
            },
            {
              id: 104,
              picture:
                'https://yanxuan-item.nosdn.127.net/56eb25a38d7a630e76a608a9360eec6b.jpg',
              name: '基础百搭,儿童套头针织毛衣1-9岁',
              tag: '儿童服饰',
            },
          ],
        }
      },
    }
    </script>
    
    <style lang="less" scoped>
    .table-case {
      width: 1000px;
      margin: 50px auto;
      img {
        width: 100px;
        height: 100px;
        object-fit: contain;
        vertical-align: middle;
      }
    }
    </style>
  • MyTable.vue

    html 复制代码
    <template>
      <table class="my-table">
        <thead>
          <tr>
            <slot name="head"></slot>
          </tr>
        </thead>
        <tbody>
          <tr v-for="(item ,index) in data" :key="item.id">
            <slot name="body" :item="item" :index="index"></slot>
          </tr>
        </tbody>
      </table>
    </template>
    
    <script>
    export default {
      props: {
        data: {
          type:Array,
          required: true
        },
      },
      components: {
      }
    }
    </script>
    
    <style  lang="less" scoped>
    .my-table {
      width: 100%;
      border-spacing: 0;
      img {
        width: 100px;
        height: 100px;
        object-fit: contain;
        vertical-align: middle;
      }
      th {
        background: #f5f5f5;
        border-bottom: 2px solid #069;
      }
      td {
        border-bottom: 1px dashed #ccc;
      }
      td,
      th {
        text-align: center;
        padding: 10px;
        transition: all 0.5s;
        &.red {
          color: red;
        }
      }
      .none {
        height: 100px;
        line-height: 100px;
        color: #999;
      }
    }
    </style>
  • MyTag.vue

    html 复制代码
    <template>
      <div class="my-tag">
        <input 
          v-if="isEdit"
          v-focus
          @blur="isEdit = false"
          ref="inp"
          class="input" 
          type="text" 
          :value="value"
          @keyup.enter="handleEnter"
          placeholder="输入标签">
        <div 
          v-else
          @dblclick="handleClick"
          class="text">
          {{value}}
        </div>
      </div>
    </template>
    
    <script>
    export default {
      data () {
        return {
          isEdit: false
        }
      },
      props: {
        value: String
      },
      methods: {
        handleClick(){
          // 双击后, 切换到显示状态
          this.isEdit = true
          // // 由于Vue是异步dom更新, 所以要等dom更新完, 再获取焦点
          // this.$nextTick(() => {
          //   // 获取焦点
          //   this.$refs.inp.focus()
          // })
        },
        handleEnter(e){
          if(e.target.value.trim() === ''){
            return alert('标签内容不能为空')
          }
          // 子传父, 将回车时, [输入框的内容] 提交给父组件更新
          // 由于父组件是v-model, 触发事件, 需要触发input事件
          this.$emit('input', e.target.value)
          this.isEdit = false
        }
      }
    }
    </script>
    
    <style lang="less" scoped>
    .my-tag {
      cursor: pointer;
      .input {
        appearance: none;
        outline: none;
        border: 1px solid #ccc;
        width: 100px;
        height: 40px;
        box-sizing: border-box;
        padding: 10px;
        color: #666;
        &::placeholder {
          color: #666;
        }
      }
    }
    </style>

路由入门

单页面应用程序

  • 单页面应用程序
    • 所有功能在一个html页面上实现
    • 优点: 按需更新性能高, 开发效率高, 用户体验好
    • 缺点: 学习成本, 首屏加载慢, 不利于SEO
    • 应用场景: 系统类/内部/文档类/移动端 网站

路由基本使用

路由的使用步骤 5 + 2
5个基础步骤
1. 下载 v3.6.5
2. 引入
3. 安装注册 Vue.use(Vue插件)
4. 创建路由对象
5. 注入到new Vue中,建立关联

2个核心步骤
1. 建组件(views目录),配规则
2. 准备导航链接,配置路由出口(匹配的组件展示的位置) 
  • App.vue

    html 复制代码
    <template>
      <div>
        <div class="footer_wrap">
          <a href="#/find">发现音乐</a>
          <a href="#/my">我的音乐</a>
          <a href="#/friend">朋友</a>
        </div>
        <div class="top">
          <!-- 路由出口 → 匹配的组件所展示的位置 -->
          <router-view></router-view>
        </div>
      </div>
    </template>
    
    <script>
    export default {};
    </script>
    
    <style>
    body {
      margin: 0;
      padding: 0;
    }
    .footer_wrap {
      position: relative;
      left: 0;
      top: 0;
      display: flex;
      width: 100%;
      text-align: center;
      background-color: #333;
      color: #ccc;
    }
    .footer_wrap a {
      flex: 1;
      text-decoration: none;
      padding: 20px 0;
      line-height: 20px;
      background-color: #333;
      color: #ccc;
      border: 1px solid black;
    }
    .footer_wrap a:hover {
      background-color: #555;
    }
    </style>
  • main.js

    js 复制代码
    import Vue from 'vue'
    import App from './App.vue'
    
    import Find from './views/Find'
    import My from './views/My'
    import Friend from './views/Friend'
    import VueRouter from 'vue-router'
    Vue.use(VueRouter) // VueRouter插件初始化
    
    const router = new VueRouter({
      // routes 路由规则们
      // route  一条路由规则 { path: 路径, component: 组件 }
      routes: [
        { path: '/find', component: Find },
        { path: '/my', component: My },
        { path: '/friend', component: Friend },
      ]
    })
    
    Vue.config.productionTip = false
    
    new Vue({
      render: h => h(App),
      router
    }).$mount('#app')

来源

黑马程序员. Vue2+Vue3基础入门到实战项目

相关推荐
桂月二二19 分钟前
探索前端开发中的 Web Vitals —— 提升用户体验的关键技术
前端·ux
CodeClimb1 小时前
【华为OD-E卷 - 第k个排列 100分(python、java、c++、js、c)】
java·javascript·c++·python·华为od
沈梦研1 小时前
【Vscode】Vscode不能执行vue脚本的原因及解决方法
ide·vue.js·vscode
hunter2062062 小时前
ubuntu向一个pc主机通过web发送数据,pc端通过工具直接查看收到的数据
linux·前端·ubuntu
qzhqbb2 小时前
web服务器 网站部署的架构
服务器·前端·架构
刻刻帝的海角2 小时前
CSS 颜色
前端·css
轻口味2 小时前
Vue.js 组件之间的通信模式
vue.js
浪浪山小白兔3 小时前
HTML5 新表单属性详解
前端·html·html5
lee5763 小时前
npm run dev 时直接打开Chrome浏览器
前端·chrome·npm
2401_897579653 小时前
AI赋能Flutter开发:ScriptEcho助你高效构建跨端应用
前端·人工智能·flutter