vue组件化开发—脚手架

96 阅读12分钟

vue组件化开发—脚手架

vue CLI脚手架

含义

  • Vue的脚手架就是Vue CLI

    • CLI是Command-Line Interface, 翻译为命令行界面;
    • 我们可以通过CLI选择项目的配置和创建出我们的项目;
    • Vue CLI已经内置了webpack相关的配置,我们不需要从零来配置;

安装

  • 安装Vue CLI

    • 我们是进行全局安装,这样在任何时候都可以通过vue的命令来创建项目:
    • npm install @vue/cli -g
  • 升级Vue CLI:

    • 如果是比较刻日的版本,可以通过下面的命令来升级
    • npm update @vue/cli -g
  • 通过Vue的命令来创建项目

    • Vue create 项目的名称

  • 项目的目录结构

什么是组件化

组件化是一种思想,将一个复杂的页面分解成多个小的组件,每个组件都是独立的个体,互不影响,这样管理和维护起来就非常的容易。组件化也是vue.js中重要的思想。

  1. 它提供了一种抽象,我们开发出一个个的组件来构成我们的应用。
  2. 每一个应用都可以抽象成一颗组件树。

CSS样式作用域

在style标签上写上scoped属性,能够将写的CSS样式局限在该组件中。

vue文件在编译后,会有自身的标识,同样的文件内写的css也会有,所以当组件相互使用的时候不将css样式设置作用域,可能会影响其他组件的样式。

基础使用

注册全局组件

<template id="my-cpn">
<h2>我是组件标题<2>
<p>我是组件内容,哈哈哈</p>
</template><script src="../js'vue.js"></script><script>
const app= Vue.createApp(App);
​
//·注册全局组件(使用app)
app.component("my-cpn"){
    template:"#my-cpn"
});
app.mount(#app');
</script>

注册局部组件(平时开发时通常都是使用局部组件。)

APP.vue

<template>
​
  <h1 class="title">我是根vue</h1>
  <hr>
    <!--使用组件-->
  <APPheader></APPheader>
  <APPheader></APPheader>
</template><script>
 
​
import APPheader from "./components/APPheader.vue"
    
    
export default {
  name: 'App',
    
    <!--注册组件-->
  components: {
    APPheader
  }
}
</script><style>
.title {
  color: bisque;
}
</style>

APPheader.vue

<!--创建组件构造器-->
<template>
    <div class="APPheader">
        <h2 class="title">我是appheader</h2>
        <hr>
    </div></template><script></script><style scoped>
.title {
    color: red;
}
</style>

组件化—组件间通信

父子组件之间通信

父组件 传递给 子组件(父传子)

定义(props

  • 什么是Props呢?

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

    • 方式一:字符串数组,数组中的字符串就是attribute的名称;

    父组件:注册两个属性(titlecontent)传递数据:

    <template>
      <div>
        <show-message title="哈哈哈" content="我是哈哈哈"></show-message>
      </div>
    </template><script>
      import ShowMessage from './ShowMessage.vue'
    ​
      export default {
        components: {
          ShowMessage
        }
      }
    </script><style scoped></style>
    

    子组件:选项中按需求注册 props和使用相关数据

    <template>
      <div>
        <h2>{{ title }}</h2>
        <p>{{ content }}</p>
      </div>
    </template><script>
      export default {
        props: ['title', 'content']
      }
    </script><style scoped></style>
    
    • 方式二:对象类型,对象类型我们可以在指定attribute名称的同时,指定它需要传递的类型、是否是必须的、默认值等等;
<template>
  <div>
    <h2>{{ title }}</h2>
    <p>{{ content }}</p>
  </div>
</template><script>
  export default {
    props: {
        title: {
            type: String,//类型
            required: true,//必要性
            default:"我是title默认值",//默认值
        },
        user:{
            type:Object,
            //对象和数组必须从一个工厂函数获得
            default(){
                return{
                    name:"yunmu",
                    age:18,
                };          
            },
        },
        //自定义验证函数
        message:{
            validator(value){
            //这个值必须匹配下面字符串的一个
            return ["success","warning","danger"].includes(value);
            },
​
        }
​
    }
  }
</script><style scoped></style>

备注:props是只读的,Vue底层会监测你对props的修改,如果进行了修改,就会发出警告,若业务需求确实需要修改,那么请复制props的内容到data中一份,然后去修改data中的数据

type的属性可以为String、Number、Boolean、Array、Object、Date、Function、Symbol

TML 中的 attribute 名是大小写不敏感的,所以浏览器会把所有大写字符解释为小写字符

使用传值时候建议kebab-case (短横线分隔命名) ,接受的时候可以为camelCase (驼峰命名法)

非Prop的Attribute

当我们传递给一个组件某个属性,但是该属性并没有定义对应的props或者emits时,就称之为 非Prop的 Attribute

常见的包括class、style、id属性等

当组件由单个根节点的时候,非Prop的Attribute将自动添加到根节点的Attribute中

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

  • 根元素不继承了,但是我们依然可以通过 $attrs来访问所有的 非props的attribute赋值给其他元素
<h2 id="$attrs.name">MultiRootElement</h2>
  • 多个根节点的attribute如果没有显示的绑定,那么会报警告,我们必须手动的指定要绑定到哪一个属性上
<h2>MultiRootElement</h2>
<h2>MultiRootElement</h2>
<h2 id="$attrs.id">MultiRootElement<h2>

子传父(通过$emit触发自定义事件)

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

我们需要在子组件中定义好在某些情况下触发的事件名称

然后在父组件中以v-on的方式传入要监听的事件名称,并且绑定到对应的方法中

最后,在子组件中触发事件,根据事件名触发对应父组件绑定的事件

父组件

<template>
  <div>
    <h2>当前计数:{{ count }}</h2>
     <!-- 使用子组件的点击事件 -->
    <child-btn @add="increment" @sub="decrement"></child-btn>
  </div>
</template><script>
import ChildBtn from "./ChildBtn.vue";
export default {
  components: {
    ChildBtn,
  },
  data() {
    return {
      count: 0,
    };
  },
  methods: {
    increment() {
      this.count++;
    },
    decrement(obj) {
      // 可以接收子组件传输来的值
      console.log(obj);
      this.count--;
    },
  },
};
</script>

子组件

<template>
  <div>
    <button @click="increment">+1</button>
    <button @click="decrement">-1</button>
  </div>
</template><script>
  export default {
    // emits是声明组件传输定义的函数名
    // emits: ["add", "sub"],
    // 对象的写法是对参数的验证
    emits: {
      add: null,
      sub: (payload) => {
        // 拦截
        console.dir(payload);
        if(payload.age >= 18) return false
        else return true
      }
    },
    methods: {
      increment() {
        this.$emit("add")
      },
      decrement() {
        let params = {
          name: "lsf",
          age: 18
        }
        // 可以传值
        this.$emit("sub", params)
      }
    },
  }
</script>

父组件给后代传值

有了组件嵌套,但是父组件给孙子组件甚至更后代的组件,仅通过props会显得特别的麻烦,这个时候可以通过 provideinject进行操作实现。

父组件在provide中添加想要传输的属性,在后代组件中能够通过inject接收对应属性键当做自己的属性使用 注意:只能在后代组件中使用

父组件

export default {
  components: { Home },
  // 提供给子孙使用,本身和兄弟或其他组件不能使用
  // 将属性写成函数形式,能够每次返回都是个新的对象:参考vue2的data
  provide() {
    return {
      name: "刘德华",
      age: 18,
      // 将length变成响应式,computed返回一个ref对象
      length: computed(() => this.names.length),
    };
  },
  data() {
    return {
      names: ["bac", "SDf", "Sfr"],
    };
  },
  methods: {
    change() {
      this.names.push('hha')
    }
  },
};

孙子组件

<template>
  <div>HomeContent: {{ name }} - {{ age }} -- {{length}}</div>
</template><script>
export default {
  inject: ["name", "age", "length"],
};
</script>

事件总线插件 mitt (兄弟组件交互)

有了父组件给孙子组件传输,但是兄弟组件或者其他没有关系的组件,无法进行自定义的数据交互(vuex是存储状态),这时就需要一个事件总线。

在vue2中使用eventBus,而在vue3中删除了该api

npm install mitt 下载插件

全局中导入,使用同一个mitt对象:

  • 在发送事件的组件中使用 emitter.emit("fnName", 值)
  • 接收事件的组件中使用 emitter.on("fnName", (type,info) => {}); fnName如果是*,则匹配所有传来的事件:type是函数名,info是信息
  • 取消事件:emitter.off(fnName)

mitt的使用:新建一个js文件,导出该对象

import mitt from 'mitt'const emitter = mitt()
​
​
// 定义一个函数,用来取消函数监听
emitter.cancelFn = (fnName) => {
  emitter.off(fnName)
}
export default emitter
复制代码

事件的发送

import emitter from "./utils/eventBus";   
change() {
      console.log("btn点击");
      emitter.emit("foo", {msg: "mitt事件"})
​
      emitter.emit("fn", {name: "fn"})
    }
复制代码

事件的接受

import emitter from "./utils/eventBus";
export default {
  created() {
    emitter.on("foo", (info) => {
      console.log(info);
    })
​
    emitter.on("*", (type, info) => {
      console.log("监听所有事件:", type, info);
    })
  },
};
​

插槽的使用

插槽能够让组件充分利用,不仅能使用组件自带的内容,还可以在父组件中diy自定义的内容,让组件复用更灵活

插槽组件

<template>
  <div class="navbar">
    <div class="left">
      <!-- 具名插槽 --> 
      <slot name="left">左边</slot>
    </div>
    <div class="center">
        <!-- 默认插槽 -->
      <slot>中间默认</slot>
    </div>
    <div class="right">
      <slot name="right">右边</slot>
    </div>
  </div>
  <!-- 名字不固定的插槽,通过变量来定义 -->
  <div class="namebox">
    <slot :name="name"></slot>
  </div>
</template><script>
export default {
  props: {
    name: String
  },
  data () {
    return {
      title: "navbar的title"
    }
  }
}
</script>

父组件使用插槽

    <!-- name需要传入,子组件才能够匹配对应插槽 -->
    <nav-bar :name="name">
      <template v-slot:right>
        <div>
          <h3>替换右边的</h3>
        </div>
      </template>
      <template v-slot:left>
        <h2>替换左边的</h2>
      </template>
      <!-- v-slot:可以缩写成 # -->
      <template #default>
        <div>替换中间默认的</div>
      </template>
      <!-- 加个[]能够插入值,替换想进入的插槽 -->
      <template #[name]>
        <div>未定义name的插槽</div>
      </template>
    </nav-bar>

插槽作用域

插槽作用域:组件是在父组件中编译定义的,数据无法使用所处组件中的数据

想获取本组件中的数据需要在组件中绑定,然后在插槽中获取到

插槽组件

<template>
  <div>
    <template v-for="item,index in names" :key="item">
      <slot :item="item" :index="index"><span>{{item}}</span> |</slot>
    </template>
  </div>
</template><script>
  export default {
    props: {
      names: {
        type: Array,
        default: () => []
      }
    }
  }
</script>

父组件使用插槽

    <data-content :names="people">
      <template v-slot="slotProps">
        <li>{{ slotProps.index + 1 }} --- {{ slotProps.item }}</li>
      </template>
    </data-content>
    <hr />
    <!-- 独占默认插槽缩写:组件中有且只有一个插槽,并且还是默认插槽时使用 -->
    <data-content :names="people" v-slot="slotProps">
      <li>{{ slotProps.index + 1 }} --- {{ slotProps.item }}</li>
    </data-content>

组件的生命周期

定义

  • 生命周期函数:

    • 生命周期函数是一些钩子函数(回调函数),在某个时间会被Vue源码内部进行回调;
    • 通过对生命周期函数的回调,我们可以知道目前组件正在经历什么阶段;
    • 那么我们就可以在该生命周期中编写属于自己的逻辑代码了;

流程

代码

<script>
    // keepalive钩子函数
  activated() {
    console.log("home 活跃状态");
  },
  deactivated() {
    console.log("home 变成非活跃状态");
  },
•
  // 正常的生命周期
  beforeCreate() {
    console.log("home beforeCreated");
  },
  created() {
    console.log("home created");
  },
  beforeMount() {
    console.log("home beforeMount");
  },
  mounted() {
    console.log("home mounted");
  },
  beforeUpdate() {
    console.log("home beforeUpdate");
  },
  updated() {
    console.log("home updated");
  },
  // vue3 将 销毁 destroy换成了卸载
  beforeUnmount() {
    console.log("home beforeUnmount");
  },
  unmounted() {
    console.log("home beforeUnmount");
  },
</script>

获取dom元素或指定组件

  1. 给组件和dom指定ref的标识
  • 某些情况下,我们在组件中想要直接获取到元素对象或者子组件实例:

    • 在Vue开发中我们是不推荐进行DOM操作的;
    • 这个时候,我们可以给元素或者组件绑定一个ref的attribute属性;
  • 组件实例有一个$refs属性:

    • 它一个对象Object,持有注册过 ref attribute 的所有 DOM 元素和组件实例。
<template>
  <div>
    <nav-bar ref="bar"></nav-bar>
    <h2 ref="title">哈哈哈</h2>
    <button @click="btnClick">获取元素</button>
  </div>
</template>
<script>
import NavBar from "./NavBar.vue";
export default {
  components: { NavBar },
  data() {
    return {
      message: "我是bar的父组件",
    };
  },
  methods: {
    btnClick() {
      // this.$refs 是一个proxy对象,存储不同的信息(DOM,组件等)
      console.log(this.$refs);
      // 获取到组件中的title值,还可以调用组件中的函数
      console.log(this.$refs.bar.title);
      this.$refs.bar.sayHello();
      this.$refs.bar.getParentAndRoot();
    },
  },
};
</script>

2.组件内部定义函数和变量

export default {
  props: {
    name: String
  },
  data () {
    return {
      title: "navbar的title"
    }
  },
  methods: {
    sayHello() {
      console.log("hello 我是bar组件");
    },
    getParentAndRoot() {
      // 获取父组件
      console.log(this.$parent.message);
      // 获取根元素
      console.log(this.$root);
    }
  },
}

动态组件

动态组件,顾名思义就是组件可以不固定写死,能够灵活变通;主要是因为使用了vue的内置组价 <component :is="name"></component> 其中的name就是组件自定义的名字

使用动态组件两种办法

  • v-if判断:使用template包裹组件进行if else判断

        <!-- 1. v-if 判断实现 -->
        <template v-if="currentTab === 'home'">
          <home></home>
        </template>
        <template v-else-if="currentTab === 'about'">
          <about></about>
        </template>
        <template v-else>
          <category></category>
        </template>
  • (推荐) 使用内置组件 component:component和使用组件无异,可以正常的传值和绑定事件。

    注意:is后面的值需要在components对象中自定义的写入

          <component
            :is="currentTab"
            name="coder"
            :age="18"
            @pageClick="pageClick"
          ></component>
    
    export default {
      components: {
        home: Home,
        About: About,
        category: Category,
      },
      data() {
        return {
          tabs: ["home", "about", "category"],
          currentTab: "home",
        };
      },
      methods: {
        itemClick(item) {
          this.currentTab = item;
        },
        pageClick() {},
      },
    };
    

keep-alive组件

keep-alive组件能够将使用过的组件缓存,不会进行销毁重建的操作,保留组件之前的行为。

例如:在组件内定义一个变量num=0,进行操作后 num变成了 8;如果没有使用keepalive缓存,则再次进入组件num值依旧为0,相当于进入组件执行了刷新的操作。

组件使用keepalive

    <!-- keepalive -->
    <!-- include 包括将被缓存的组件,内部变量是组件的name值(和data同级的属性) -->
    <!-- exclude 不被缓存的组件内 -->
    <keep-alive :include="['home']">
      <component
        :is="currentTab"
        name="coder"
        :age="18"
        @pageClick="pageClick"
      ></component>
    </keep-alive>
复制代码

使用了keepalive的组件,不会进行销毁重建的生命周期,它拥有自己的激活钩子函数

动态组件的<keep-alive></keep-alive>第一次进入会进入创建的周期,其他都是会在自身的生命周期,或更新时调用 update的钩子函数

  • 激活 activated()
  • 失活 deactivated()

异步组件

  1. webpack中分包处理

     import('./posts/posts').then(({ default: posts }) => {
          mainElement.appendChild(posts())
        })
    
  2. vue中异步组件的使用

    <template>
      <div>
        异步组件
        <suspense>
          <!-- 加载完毕显示的组件 -->
          <template #default>
            <async-category></async-category>
          </template>
          <!-- 默认组件未加载,占位组件 -->
          <template #fallback>
            <loading></loading>
          </template>
        </suspense>
      </div>
    </template>
    <script>
    // vue3提供的加载异步组件的函数,接收一个方法,返回一个promise
    import { defineAsyncComponent } from "vue";
    // import AsyncCategory from './AsyncCategory.vue';
    // import方法,会让该组件延迟加载,不会统一打包在app.js文件中,而是使用的时候引入进来
    // 写法一:常用写法,接受一个函数
    const AsyncCategory = defineAsyncComponent(() => import("./AsyncCategory.vue"))
    // 写法二:接受一个对象
    // const AsyncCategory = defineAsyncComponent({
    //   loadingComponent: Loading, // 当异步组件未加载的时候显示该组件
    //   loader: () => import("./AsyncCategory.vue"),
    //   // errorComponent, // 出错时显示的组件
    //   delay: 2000, // 在显示loadingComponent组件之前,等待多长时间
    //   /**
    //    * err: 错误信息
    //    * retry: 函数,调用retry尝试重新加载
    //    * fail: 函数,提示加载程序结束退出
    //    * attempts:记录尝试的次数
    //    */
    //   onError: function (err, retry, fail, attempts) {},
    // });
    import Loading from "./Loading.vue";
    ​
    export default {
      components: {
        AsyncCategory,
        Loading,
      },
    };
    
    • 模拟异步
    <template>
      <div>
        asyncCategory --- {{ten}}
      </div>
    </template><script setup>
    import {ref} from "vue"
    function awaitMe () {
      return new Promise(resolve => {
        setTimeout(() => {
          resolve(10)
        }, 3000)
      })
    }
    const num = await awaitMe()
    let ten = ref(num)
    </script>
    

    组件的v-model

v-model 就是一个语法糖,vue中帮我们将绑定属性和方法使用v-model指令完成了;而其中内部做的事情需要我们探究一下,能够更好的使用。

v-model语法糖

    <!-- <input v-model="message"> -->
    <!-- 语法糖做的事情 -->
    <!-- <input :value="message" @input="message = $event.target.value"> -->
    
    <!-- 在组件上使用v-model -->
    <!-- 绑定单个 v-model -->
    <!-- <my-input v-model="message"></my-input> -->
    <my-input v-model="message" v-model:title="title"></my-input>
    <!-- vue3内部帮忙做的事情 -->
    <!-- <my-input :modelValue="message" @update:modelValue="message = $event"></my-input> -->

组件内配合v-model,需要做的事情

export default {
  // 不建议直接绑定到props里面的属性,使用computed最好
  props: ["modelValue", "title"],
  emits: ["update:modelValue", "update:title"],
  computed: {
    inputValue: {
      set(value) {
        this.$emit("update:modelValue", value);
      },
      get() {
        return this.modelValue;
      },
    },
    myTitle: {
      set(value) {
        this.$emit("update:title", value);
​
      },
      get() {
        return this.title
      }
    }
  },
};

mixin使用

Vue中的mixin

先来看一下官方定义

mixin(混入),提供了一种非常灵活的方式,来分发 Vue 组件中的可复用功能。

本质其实就是一个js对象,它可以包含我们组件中任意功能选项,如datacomponentsmethodscreatedcomputed等等

我们只要将共用的功能以对象的方式传入 mixins选项中,当组件使用 mixins对象时所有mixins对象的选项都将被混入该组件本身的选项中来

Vue中我们可以局部混入全局混入

局部混入

定义一个mixin对象,有组件optionsdatamethods属性

var myMixin = {
  created: function () {
    this.hello()
  },
  methods: {
    hello: function () {
      console.log('hello from mixin!')
    }
  }
}

组件通过mixins属性调用mixin对象

Vue.component('componentA',{
  mixins: [myMixin]
})

该组件在使用的时候,混合了mixin里面的方法,在自动执行created生命钩子,执行hello方法

全局混入

通过Vue.mixin()进行全局的混入

Vue.mixin({
  created: function () {
      console.log("全局混入")
    }
})

使用全局混入需要特别注意,因为它会影响到每一个组件实例(包括第三方组件)

PS:全局混入常用于插件的编写

注意事项:

当组件存在与mixin对象相同的选项的时候,进行递归合并的时候组件的选项会覆盖mixin的选项

但是如果相同选项为生命周期钩子的时候,会合并成一个数组,先执行mixin的钩子,再执行组件的钩子

使用场景

在日常的开发中,我们经常会遇到在不同的组件中经常会需要用到一些相同或者相似的代码,这些代码的功能相对独立

这时,可以通过Vuemixin功能将相同或者相似的代码提出来

举个例子

定义一个modal弹窗组件,内部通过isShowing来控制显示

const Modal = {
  template: '#modal',
  data() {
    return {
      isShowing: false
    }
  },
  methods: {
    toggleShow() {
      this.isShowing = !this.isShowing;
    }
  }
}

定义一个tooltip提示框,内部通过isShowing来控制显示

const Tooltip = {
  template: '#tooltip',
  data() {
    return {
      isShowing: false
    }
  },
  methods: {
    toggleShow() {
      this.isShowing = !this.isShowing;
    }
  }
}

通过观察上面两个组件,发现两者的逻辑是相同,代码控制显示也是相同的,这时候mixin就派上用场了

首先抽出共同代码,编写一个mixin

const toggle = {
  data() {
    return {
      isShowing: false
    }
  },
  methods: {
    toggleShow() {
      this.isShowing = !this.isShowing;
    }
  }
}

两个组件在使用上,只需要引入mixin

const Modal = {
  template: '#modal',
  mixins: [toggle]
};
 
const Tooltip = {
  template: '#tooltip',
  mixins: [toggle]
}

通过上面小小的例子,让我们知道了Mixin对于封装一些可复用的功能如此有趣、方便、实用