前端框架Vue(二)


组件

组件的定义

组件允许我们将UI划分为独立的、可重用的部分,并可以对每个部分进行单独的思考。在实际应用中,组件常常被组织成层层嵌套的树状结构:

这和我们嵌套HTML元素的方式类似,Vue实现了自己的组件模型,使我们可以在每个组件内封装自定义内容与逻辑。

<body>
  <div id="root">
    <pl-navbar></pl-navbar>
  </div>
  <script src="../vue.js"></script>
  <script>
    const app = Vue.createApp({
      data(){
        return{

        }
      },
    })
    // 定义组件
    app.component("pl-navbar",{
      // 模板(使用键盘上1前方的`符号进行包裹)
      template:`  
        <nav>
          <ul>
            <li>首页</li>
            <li>新闻中心</li>
            <li>产品大全</li>
          </ul>
        </nav>
      `
    })
    app.mount("#root")
  </script>
</body>

预览结果如下:

注意:由于HTML中不区分英文字母大小写,所以定义组件名称时可以用驼峰式命名plNavbar,但在使用组件时,需要通过-连接的名称pl-navbar。或者组件名、变量名均用不带任何字符的小写字母

全局组件和局部组件

<body>
  <div id="root">
    <pl-navbar></pl-navbar>
    <pl-sidebar></pl-sidebar>
  </div>
  <script src="../vue.js"></script>
  <script>
    const app = Vue.createApp({
      data(){
        return{

        }
      },
    })
    // 定义全局组件
    app.component("pl-navbar",{
      // 模板(使用键盘上1前方的`符号进行包裹)
      template:`  
        <nav style="background:yellow;">
          <ul>
            <li v-for="item in datalist">
              {{item}}
            </li>
          </ul>
        </nav>
      `,
      data(){
        return{
          datalist:["首页","新闻中心","产品大全"]
        }
      }
    })
    // 全局组件定义
    app.component("pl-sidebar",{
      template:`
        <aside>
          我是侧边栏
          <pl-button></pl-button>
        </aside>
      `,
      // 局部组件定义
      components:{
        "pl-button":{
          template:`
            <div style="background:red;">
              <button style="background:red;">联系</button>
            </div>
          `,
          // watch,computed,methods
        }
      }
    })
    app.mount("#root")
  </script>
</body>

上面的例子是将所有组件都写入在HTML中,而Vue在实际使用时,会将组件提取到单独的组件文件中,即*.vue文件,英文Single-File Component,简称SFC,单文件组件。这个是一个特殊的文件格式,将一个Vue组件的模板、逻辑与样式封装在单个文件中。

(前面的例子中,都是将vue代码写入到html文件中,并引入vue的CDN文件进行解释,而单文件组件,需要新建*.vue文件,而浏览器默认无法识别vue文件,也无法通过引入vue的CDN文件的形式,需要进行vue环境的安装,见本小节的附录)

组件间的通信

父传子

语法格式:

  • 父组件绑定数据传入

    v-bind:attr1="xxxx" v-bind:attr2="yyyy"

  • 子组件声明接收

    props:["attr1","attr2"]

父组件:App.vue

<template>
  <div class="root">
    <Navbar left="返回" title="我的产品列表页" right="首页"></Navbar>
  </div>
</template>

<script>
  import Navbar from './components/Navbar.vue'
  import Layout from './components/Layout.vue'
  export default{
    components:{
      Navbar,
      Layout
    }
  }
</script>

子组件:Navbar.vue

<template>
  <div>
    <button>{{left}}</button>
    {{title}}
    <button>{{right}}</button>
  </div>
</template>

<script>
  export default{
    props:["title","left","right"]
  }
</script>

预览页面:

其中父组件中的

<Navbar left="返回" title="我的产品列表页" right="首页"></Navbar>

可以做如下改写:

<template>
  <div class="root">
    <Navbar v-bind="propnav"></Navbar>
  </div>
</template>

<script>
  import Navbar from './components/Navbar.vue'
  import Layout from './components/Layout.vue'
  export default{
    data(){
      return{
        propnav:{
            left:'返回',
            title:'我的产品列表页',
            right:'首页'
        }
      }
    },
    components:{
      Navbar,
      Layout
    }
  }
</script>

所有的props都遵循着单向绑定原则,props因父组件的更新而变化,自然地将新的状态向下流往子组件,而不会逆向传递。

属性验证

props接收时可以指定特定的类型或指定范围值

<template>
  <div>
    <button v-show="leftshow">{{left}}</button>
    {{title}}
    <button v-show="rightshow">{{right}}</button>
  </div>
</template>

<script>
  export default{
    props:{
      title:String,
      left:[String,Number],
      right:{
        required:true, // 必传,不设置成必传。不传这个值就不会经过校验器进行正确性校验
        // 校验器
        Validator(value){
          return ['success','waring','danger'].includes(value)
        }
      },
      leftshow:{
        type:Boolean,
        default:true  // 设置默认值
      },
      rightshow:Boolean
    }
  }
</script>

子传父

父组件App.vue

<template>
  <div class="root">
    <Navbar left="返回" :title="title" right="首页"
    :leftshow="true" :rightshow="false"
    ></Navbar>
    <!-- <Navbar v-bind="propnav"></Navbar> -->
    <button @click="plClick">click</button>
    <Layout @event="$event => plEvent($event)"></Layout>
  </div>
</template>

<script>
  import Navbar from './components/Navbar.vue'
  import Layout from './components/Layout.vue'
  export default{
    data(){
      return{
        title:"我的产品列表页",
        propnav:{
            // left:'返回',
            mytitle:'我的产品列表页',
            // right:'首页'
        }
      }
    },
    methods:{
      plClick(){
        this.title="我的产品列表页-第一页"
      },
      plEvent(data){
        console.log("app-event",data)
      }
    },
    components:{
      Navbar,
      Layout
    }
  }
</script>

子组件Layout.vue

<template>
  <div>
    Layout-child
    <button @click="plClick">click</button>
  </div>
</template>

<script>
  export default{
    data(){
      return{
        childtitle:"child-1111"
      }
    },
    methods:{
      plClick(){
        // console.log(this.childtitle)
        this.$emit("event",this.childtitle)
      }
    }
  }
</script>

<style scoped>
  div{
    background: green;
  }
</style>

上面的例子中,父组件中定义的@event为自定义事件,名称可以任意取名,@自定义事件名="表达式或方法";子组件触发自定义事件$emit("自定义事件名")

$refs

  • ref如果绑定在dom节点上,得到的就是原生dom节点
  • ref如果绑定在组件上,得到的就是组件对象,可以实现通信功能

例子:

父组件App.vue

<template>
  <div>
    app-<button @click="plClick()">click</button>
    <!-- 原生dom -->
    <input ref="myinput"/>
    <div ref="mydiv"></div>
    
    <!-- 新建一个子组件Child.vue -->
    <Child ref="mychild"></Child>
  </div>
</template>

<script>
// 引入子组件Child.vue
import Child from './Child.vue'
  export default{
    // 注册子组件
    components:{
      Child
    },
    methods:{
      plClick(){
        // 原生dom
        console.log(this.$refs.myinput)
        console.log(this.$refs.mydiv)
        // 组件,可以获取到子组件的对象及属性
        console.log(this.$refs.mychild.childtitle)
        
        console.log(this.$refs)
        // 修改子组件属性值
        this.$refs.mychild.childtitle = "22222"
      }
    }
  }
</script>

子组件Child.vue

<template>
  <div>
    child-{{childtitle}}
  </div>
</template>

<script>
  export default{
    data(){
      return {
        childtitle:"11111"
      }
    }
  }
</script>

$parent$root

$parent父组件,$root根组件

例子:

根组件 App.vue

<template>
  <div>
    app
    <!-- 这里引入了一个组件Aparent,即Aparent是当前组件App的子组件 -->
    <Apartent></Apartent>
  </div>
</template>

<script>
  import Apartent from './Apartent.vue';
  export default{
    data(){
      return{
        title:"根组件root"
      }
    },
    components:{
      Apartent
    }
  }
</script>

App的子组件Aparent

<template>
  <div>
    parent
    <!-- 这里引入了一个组件BChild,即BChild是当前组件Apartent的子组件 -->
    <BChild/>
  </div>
</template>
<script>
import BChild from './BChild.vue';
  export default{
    data(){
      return{
        title:"父组件parent"
      }
    },
    components:{
      BChild
    }
  }
</script>

Aparent组件的子组件BChild

<template>
  <div>
    child-<button @click="plClick">click</button>
  </div>
</template>
<script>
  export default{
    methods:{
      plClick(){
        console.log(this.$parent.title)
        console.log(this.$root.title)
      }
    }
  }
</script>

小节:父子组件间的通信有如下方法:

  • 父组件向子组件传递数据,props属性绑定;
  • 子组件向父组件传递数据,子组件触发父组件方法,通过vue实例方法vm.$emit实现
  • 使用$refs,实现父组件访问子组件的数据和方法
  • 使用$parent,实现子组件访问父组件的数据和方法

订阅发布模式

在项目中新建一个store.js

export default {
  datalist:[],
  // 订阅者
  subscribe(cb){
      this.datalist.push(cb)
      console.log(this.datalist)
  },
  // 发布者
  publish(value){
      this.datalist.forEach(cb=>cb(value))
  }
}

通过引入订阅发布模式实现组件间的数据传递

Navbar.vue

<script>
import store from './store'

export default {
  // inject:["navTitle","app"]
  
  //生命周期-mounted()
  data(){
      return {
          title:"首页"
      }
  },
  mounted(){
      //订阅,,,,
      store.subscribe((value)=>{
          console.log("我被触发了",value)
          this.title = value
      })
  }
}
</script>

TabbarItem.vue

<script>
import store from './store';

export default {
  props:["item"],
  // inject:["navTitle","app"],
  methods:{
      plClick(){
          // console.log(this.navTitle)
          store.publish(this.item)
      }
  }
}
</script>

动态组件

App.vue

<template>
  <div>
      <Navbar />
      <!-- <Home></Home>
      <List></List>
      <Center></Center> -->
      <!-- 使用keep-alive或KeepAlive实现缓存当前数据(比如输入框中输入内容,切换到其他tab,再切换回来,数据仍显示), -->
      <!-- include表示包含哪些组件,exclude表示排除哪些组件 -->
      <!-- component实现动态组件 -->
      <keep-alive include="Home,List">
        <component :is="which"></component>
      </keep-alive>
      <Tabbar/>
  </div>
</template>

<script>
import Navbar from './Navbar.vue';
import Tabbar from './Tabbar.vue';
import Home from './views/Home.vue';
import List from './views/List.vue';
import Center from './views/Center.vue';
import store from './store';

export default {
  data(){
      return {
          navTitle:"首页",
          which:"Home"
      }
  },
  provide(){
      return {
          navTitle:this.navTitle,
          app:this
      }        
  },
  mounted(){
    let obj = {
      "首页":"Home",
      "列表":"List",
      "我的":"Center"
    }
    store.subscribe((value)=>{
      this.which = obj[value]
    })
  },
  components:{
      Navbar,
      Tabbar,
      Home,
      List,
      Center
  }
}
</script>

Home.vue

<template>
  <div>
    Home
    <input/>
  </div>
</template>

<script>
export default{
  name:"Home"
}
</script>

List.vue和Center.vue与Home.vue的内容类似。

组件中的v-model

未应用组件中的v-model的例子

App.vue

<template>
  <div>
    {{plvalue}}
    <!-- 属性值的动态绑定 v-model -->
    <!-- <input type="text" v-model="plvalue"> -->
    <!-- 属性值的动态绑定,v-bind,由于v-bind是单向绑定,需要在增加事件监听 -->
    <!-- <input type="text" :value="plvalue" @input="plInput"> -->
    <!-- 也可以通过直接将表达式写在监听事件中 -->
    <!-- <input type="text" :value="plvalue" @input="plvalue=$event.target.value"> -->


    <!-- 使用子组件Field,plvalue与data()中的plvalue对应,是用户名的属性值 -->
    <!-- @plevent进行监听子组件(这里@plevent是监听事件名,“plEvent”是监听方法名) -->
    <Field label="用户名" :value="plvalue" @plevent="plEvent"/>
    <button @click="plRegister">注册</button>
    <button @click="plReset">重置</button>
  </div>
</template>

<script>
// 引入自定义组件Field
import Field from './Field.vue'
export default {
  data() {
    return {
      plvalue:"aaaa"
    }
  },
  // 注册子组件Field
  components: {
    Field
  },
  methods: {
    // plEvent被触发即value有新的值从子组件传递过来,赋值给父组件的plvalue
    plEvent(value){
      console.log(value)
      this.plvalue = value
    },
    // plInput(event) {
    //   console.log(event.target.value)
    //   this.plvalue = event.target.value
    // },
    plRegister() {
      console.log("register",this.plvalue)
    },
    plReset() {
      console.log("reset")
      this.plvalue=""
    }
  }
}
</script>

子组件Field.vue

<template>
  <div style="background: yellow;">
    
      <label>{{label}}</label>:
      
      <!-- 动态绑定value值,value来自props中的value,由父组件传递过来 -->
      <!-- @input监听输入框,当输入框的value有变化时,触发plInput方法 -->
      <input :type="type" :value="value" @input="plInput">
  </div>
</template>
<script>
export default {
  data(){
      return {
          myvalue:""
      }
  },
  methods:{
    // 当input框的value值发生变化就会触发plInput方法,通过$emit传递给父组件的plevent进行接收
    plInput(event){
      console.log(event.target.value)
      this.$emit("plevent",event.target.value)
    }
  },
  props:{
      label:{
          type:String,
          default:""
      },
      type:{
          type:String,
          default:"text"
      },
      // 增加一项父组件中需要支持的value类型,进行接收
      value:{
        type:String,
        default:""
      }
  }
}
</script>
  1. 在父组件中引入子组件Field
  2. 增加注册和重置按钮,来操作label的属性值value
  3. 在script中的data中返回value值
  4. 动态绑定value值
  5. 在子组件中的props中增加value属性值的接收验证
  6. 接收到的value值传给子组件中input绑定的属性值value
  7. 子组件的input输入框的值有变化时要传给父组件,在子组件的input增加监听事件plInput,并在methods中增加plInput()方法,获取到输入框属性值的内容event.target.value
  8. 在父组件中增加对子组件的监听事件plEvent
  9. 当子组件的input属性value有变化时,会触发子组件的监听事件plInput,由plInput通过this.$emit("plevent",event.target.value)传递给父组件的监听事件plEvent,父组件的plEvent会将子组件传递过来的value值重新赋值给前端的plvalue

下面是在组件上绑定v-model实现同样功能的例子:

App.vue

<template>
  <div>
    {{plvalue}}
    <!-- 将v-model应用在Field组件上,子组件props中的value属性的名称需要写成modelValue -->
    <Field label="用户名" v-model="plvalue"/>

    <button @click="plRegister">注册</button>
    <button @click="plReset">重置</button>
  </div>
</template>

<script>
// 引入自定义组件Field
import Field from './Field.vue'
export default {
  data() {
    return {
      plvalue:"aaaa"
    }
  },
  // 注册子组件Field
  components: {
    Field
  },
  methods: {
    // plEvent被触发即value有新的值从子组件传递过来,赋值给父组件的plvalue
    plEvent(value){
      console.log(value)
      this.plvalue = value
    },
    plRegister() {
      console.log("register",this.plvalue)
    },
    plReset() {
      console.log("reset")
      this.plvalue=""
    }
  }
}
</script>

Field.vue

<template>
  <div style="background: yellow;">
    
      <label>{{label}}</label>:
      
      <!-- v-model绑定组件时,value属性的名称需要写成modelValue  -->
      <input :type="type" :value="modelValue" @input="plInput">
  </div>
</template>
<script>
export default {
  data(){
      return {
          myvalue:""
      }
  },
  methods:{
    plInput(event){
      console.log(event.target.value)
      // v-model绑定组件时,传递给父组件时需要将plvalue改写成update:modelValue
      this.$emit("update:modelValue",event.target.value)
    }
  },
  props:{
      label:{
          type:String,
          default:""
      },
      type:{
          type:String,
          default:"text"
      },
      // v-model绑定组件时,value属性的名称需要写成modelValue
      modelValue:{
        type:String,
        default:""
      }
  }
}
</script>

异步组件

之前写的代码,都是在首次访问时,一起全部加载完成。若文件较大较多时,会出现页面加载慢的问题,这时需要对组件进行异步加载处理,当用到时才请求加载。

<script>
import { defineAsyncComponent } from 'vue';
import Navbar from './Navbar.vue';
import Tabbar from './Tabbar.vue';
// import Home from './views/Home.vue';
// import List from './views/List.vue';
// import Center from './views/Center.vue';
import store from './store';

export default {
  data(){
      return {
          navTitle:"首页",
          which:"Home"
      }
  },
  provide(){
      return {
          navTitle:this.navTitle,
          app:this
      }        
  },
  mounted(){
    let obj = {
      "首页":"Home",
      "列表":"List",
      "我的":"Center"
    }
    store.subscribe((value)=>{
      this.which = obj[value]
    })
  },
  components:{
      Navbar,
      Tabbar,
      Home:defineAsyncComponent(()=>import("./views/Home.vue")),
      List:defineAsyncComponent(()=>import("./views/List.vue")),
      Center:defineAsyncComponent(()=>import("./views/Center.vue"))
  }
}
</script>

之前的代码都是将组件一起全都导入,这里需要通过使用defineAsyncComponent()方法进行包裹导入,这样在不请求的组件不会加载,只有访问了才加载。

注:若异步加载的组件文件较大较慢,可以在defineAsyncComponent()的基础上增加加载loading及错误提示等。

附录:安装Vue

安装vue有两种方法:

Vue CLI (不再推荐)

Vue CLI已经进入维护阶段,到2023年12月31日将停止维护。

Vue ClI是一个基于Vue.js进行快速开发的完整系统,提供:

  • 通过@vue/cli实现的交互式的项目脚手架;
  • 通过@vue/cli+@vue/cli-service-global实现的零配置原型开发;
  • 一个运行时依赖(@vue/cli-service),该依赖:
    • 可升级;
    • 基于webpack构建,并带有合理的默认配置;
    • 可以通过项目内的配置文件进行配置;
    • 可以通过插件进行扩展;
  • 一个丰富的官方插件集合,集成了前端生态中最好的工具;
  • 一套完全图形化的创建和管理Vue.js项目的用户界面

安装:

npm install -g @vue/cli

注意,有可能会出现权限不够的问题,报错信息如下:

npm ERR! code EACCES
npm ERR! syscall mkdir
npm ERR! path /Users/laobai/.npm/_cacache/content-v2/sha512/a7/b1
npm ERR! errno EACCES
npm ERR! 
npm ERR! Your cache folder contains root-owned files, due to a bug in
npm ERR! previous versions of npm which has since been addressed.
npm ERR! 
npm ERR! To permanently fix this problem, please run:
npm ERR!   sudo chown -R 501:20 "/Users/laobai/.npm"

npm ERR! A complete log of this run can be found in:
npm ERR!     /Users/laobai/.npm/_logs/2023-07-16T10_49_31_741Z-debug-0.log

目录‘path /Users/laobai/.npm’权限不够,根据提示执行“sudo chown -R 501:20 “/Users/laobai/.npm””即可解决。

sudo chown -R 501:20 "/Users/laobai/.npm"

确认已安装成功

vue -V                                   
@vue/cli 5.0.8

创建vue项目

vue create vue_test01

选择Default([Vue 3] babel, eslint)进行安装 (其中babel是为了使低端浏览器(IE浏览器,其他特低版本其他浏览器(电脑端和手机端))兼容ES6,将ES6转成ES5)

创建好项目后,可以在查看项目目录中的package.json确认项目信息

{
  "name": "vue_test01",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "serve": "vue-cli-service serve",
    "build": "vue-cli-service build",
    "lint": "vue-cli-service lint"
  },
  "dependencies": {
    "core-js": "^3.8.3",
    "vue": "^3.2.13"
  },
  "devDependencies": {
    "@babel/core": "^7.12.16",
    "@babel/eslint-parser": "^7.12.16",
    "@vue/cli-plugin-babel": "~5.0.0",
    "@vue/cli-plugin-eslint": "~5.0.0",
    "@vue/cli-service": "~5.0.0",
    "eslint": "^7.32.0",
    "eslint-plugin-vue": "^8.0.3"
  }
}

可以在命令行中,执行下面的命令启动服务

npm run serve

> vue_test01@0.1.0 serve
> vue-cli-service serve

 INFO  Starting development server...


 DONE  Compiled successfully in 801ms                                                 05:35:28


  App running at:
  - Local:   http://localhost:8080/ 
  - Network: http://192.168.18.199:8080/

  Note that the development build is not optimized.
  To create a production build, run npm run build.

通过http://localhost:8080/ 访问项目页面

Vite(推荐)

Vite(法语意为“快速的”,发音/vit/)是一种新型前端构建工具,能够显著提升前端开发体验,它主要由两部分组成:

  • 一个开发服务器,它基于原生ES模块提供了丰富的内建功能,如速度快到惊人的模块热更新(HMR);
  • 一套构建指令,它使用Roollup打包你的代码,并且它是预配置的,可输出用于生产环境的高度优惠过的静态资源。

通过Vite创建vue项目

npm create vite@latest

或者

npm init vite@latest

创建时,若本地还没有vite会提示进行下载

npm create vite@latest 
✔ Project name: … vue_test02
✔ Select a framework: › Vue
✔ Select a variant: › TypeScript

Scaffolding project in /Users/laobai/TempProjects/vue_test02...

Done. Now run:

  cd test_vue02
  npm install
  npm run dev

创建了项目,并没有安装插件及工具,按照提示进入到项目目录,执行npm install进行安装

  cd test_vue02    
  npm install           

added 43 packages, and audited 44 packages in 19s

4 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities

可以在查看项目目录中的package.json确认项目信息

{
  "name": "vue_test02",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "preview": "vite preview"
  },
  "dependencies": {
    "vue": "^3.3.4"
  },
  "devDependencies": {
    "@vitejs/plugin-vue": "^4.2.3",
    "vite": "^4.4.5"
  }
}

可以在命令行中,执行下面的命令启动服务

npm run dev           

> test_vue@0.0.0 dev
> vite


  VITE v4.4.4  ready in 373 ms

  ➜  Local:   http://localhost:5202/
  ➜  press h to show help
20:23:52 [vite] vite.config.ts changed, restarting server...

通过http://localhost:5202/ 访问项目页面


文章作者: 老百
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 老百 !
  目录