前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Vue核心api和组件开发实践

Vue核心api和组件开发实践

作者头像
一粒小麦
发布2019-07-18 17:05:46
1.9K0
发布2019-07-18 17:05:46
举报
文章被收录于专栏:一Li小麦一Li小麦

本项目将在GitHub上维护更新。

https://github.com/dangjingtao/FeRemarks


看完本讲内容,大多数前端初学者就会自以为是地可以在简历写自己"熟练(精通)vue开发“了,最不济也会给自己加个“熟悉掌握vue业务逻辑”的帽子。而对这门课程来说,一切刚刚开始。

1. vue核心api:以购物车为例

**需求:**实现一个购物车(cart)

首先通过vue-cli新建一个项目。

然后在page下面做一个shop.vue,在路由中注册该页面。即可在上面做修改。

数据(插值)绑定:双大括号
代码语言:javascript
复制
data(){
	return {
		title:'cart'
	}
}

// 应用
<h1>{{title}}</h1>
属性绑定(:xxx)
代码语言:javascript
复制
<h1 :title="title">{{title}}</h1>

效果如下:

指令
  • v-if:v-if=布尔值,为真时才展示

你可以给h1加个isConsumer的布尔值:

代码语言:javascript
复制
<h1 :title="title" v-if="isConsumer">{{title}}</h1>

如果为false,就不会渲染了。

  • v-for:直接挂载在子元素上,如v-tor="item in items"
代码语言:javascript
复制
<table  class="table">
      <tbody>
        <tr >
          <th width="10%">id</th>
          <th width="50">name</th>
          <th width="20%">price</th>
        </tr>

        <tr v-for="item in tableData" :key="item.id">
          <td>{{item.id}}</td>
          <td>{{item.name}}</td>
          <td>{{item.price}}</td>
        </tr>
      </tbody>
    </table>
    
    // ...
    
 data() {
    return {
      // ...
      tableData:[
        {id:1,name:'iphone x',price:'699'},
        {id:2,name:'macbook pro',price:'1699'},
        {id:3,name:'ipad',price:'399'},
      ]
    };
  }

如下图

if比for优先级高。不建议两个一起写在同一个标签上。

用户输入(表单)

通过v-model实现"双向"绑定。

代码语言:javascript
复制
<input type="text" v-model="goods">

实际上是一个语法糖。

请求数据的时机:created和mounted

created运行时,还未挂载到DOM,不能访问到$el属性,可用于初始化一些数据,但和DOM操作相关的不能在created中执行;monuted运行时,实例已经挂在到DOM,此时可以通过DOM API获取到DOM节点。

建议在created阶段请求。除非要用到dom操作。

事件处理
代码语言:javascript
复制

<button @click="submit">

methods:{
	submit:function(){
		// 校验和处理逻辑
	}
}

和react不同,你可以写`@click="submit(data)"直接传参,无需做任何处理。

默认来说要传事件e,可以bbb(e,data)

或者$event

练习:完整实现购物车

实现购物车,有上架商品车功能。

添加商品列表

写一个表单,加点样式:

代码语言:javascript
复制
    <div class="form">
      <div class="form-item">
        <div style="width:40%;text-align:right;">trade name&nbsp;&nbsp;</div>
        <input class="input" type="text" v-model="goods" placeholder="please type the trade name" /> 
      </div>
      <div class="form-item">
        <div style="width:40%;text-align:right;">price&nbsp;&nbsp;</div>
        <input class="input" type="number" v-model="price" placeholder="please type the price" /> 
      </div>

      <div class="form-item">
        <button style="display::block;width:90%;margin:auto;" @click="submit">confirm</button>
      </div>
    </div>
// ...

核心方法(添加列表)

代码语言:javascript
复制
methods:{
    submit:function(){
      if(this.goods){
        this.tableData.push({
          id:this.tableData.length+1,
          name:this.goods,
          price:this.price,
          numbers:1
        });
        this.goods='';
        this.price='';
      }
    },
    // ...
}
添加购物车

这里有个加入购物车的按钮,新建一个表格,绑定cartlist。

代码语言:javascript
复制
<table class="table">
    <tbody>
      <tr>
        <th width="10%">id</th>
        <th width="20">name</th>
        <th width="20%">unit-price</th>
        <th width="30%">count</th>
        <th width="20%">price</th>
      </tr>

      <tr v-for="item in cartlist" :key="item.id">
        <td>{{item.id}}</td>
        <td>{{item.name}}</td>
        <td>${{item.price}}</td>
        <td>{{item.numbers}}</td>
        <td>${{item.numbers*item.price}}</td>
      </tr>
    </tbody>
  </table>

回顾购物车逻辑:

  • 当不存在,加入购物车
  • 当存在,numbers+1
  • 根据单价计算每个商品花费

给add加个处理函数:

代码语言:javascript
复制
add:function(item){
      // 深度拷贝
      item=JSON.parse(JSON.stringify(item));
      let _index=this.cartlist.map(x=>x.id).indexOf(item.id);
      if(_index<0){
        item.numbers=1;
        this.cartlist.push({...item,numbers:1})
      }else{
        this.cartlist[_index].numbers+=1;
      }      
    }

对于es6比较熟悉的话,你也可以用find方法简单查重。

代码语言:javascript
复制
this.cartlist.find(x=>x.id==item.id)
计算总价

给表格加一个tfoot


2. 组件化

在上面的实现中,我们用了类似element ui的组合方式(form/formItem)。 通用组件:常见的ui库。业务组件:平时自己写的。便于复用维护 那么组件通信有什么花式呢?

组件化工作从重构开始。

组件由重构开始

把cart的相关方法和数据都放到单独的文件中:在components下新建一个Cart.vue

在引入时,关注引入和注册:

代码语言:javascript
复制
import Cart from './../components/Cart.vue';
export default {
  name: "App",
  components: {
      Cart,
  },
  //...

那么就可以在shop下的template中引用这个组件了。

现在又个问题:shop页面下的添加到购物车(add to cart)绑定了一个处理逻辑。如何教给子组件去使用这个方法呢?

属性传参

注册属性,数据默认类型和值

代码语言:javascript
复制
// shop.vue
<cart :add="add"></cart>

// cart.vue
export default {
  props: {
      add: {
          type: Function,//默认数据类型
          default: ()=>{} //默认值
      },
  },

在子组件中直接修改属性,也会触发父组件对数据视图的重新渲染。

add是被传过来了,但是add里的this不是指向组件内的this,而是shop内的this。所以该方案不能满足业务需求。

ref传参(不推荐但总是会用)

ref方案是获取组件的真实节点。这样就获得了item和购物车组件内的add方法。

代码语言:javascript
复制
// shop.vue
<cart ref="cart"></cart>

	// add逻辑
	this.$refs.cart.add(item)

此方案缺点是耦合。因为操作dom而不被推荐。

派发事件
总线模式

项目的main.js,也就是在vue初始化时,设置一个新的"bus"方法。

代码语言:javascript
复制
// main.js
Vue.prototype.bus=new Vue()

// shop.vue 派发事件
add:function(item){
	this.$bus.$emit.on('add',item)
}

// cart.vue 响应事件
created(){
	this.$bus.$emit.on('add',item=>{
		this.add(item)
	})
}

在这个过程中,bus创建了一个新的vue实例,所有页面/组件都能访问到。父组件向全局派发了一个名为add的自定义事件,同时带上了参数item,关心这个事件的子组件(cart.vue)接受了add事件和参数,就可以在组件内部进行处理了。

通过这种方法,可以以解耦合的方式实现完全不相干的两个组件传值。但是不好之处在于:多了一个全局的Vue实例。

子组件传父组件

设想这么一个场景,假如购物单里的东西是限量发行的,用户可以买任意n(n<=3)种,但也只能3个。多了不给。

此需求的业务逻辑是:子组件传参成功后,需要通知父组件一个消息,父组件需要判断来决定是否添加(购物车为空,允许购买,购物车本商品已经达到上限,不让购买)

还是派发事件。

子组件中,设置一个count值,在处理方法add中,处理完之后,给父组件派发一个事件

代码语言:javascript
复制
		add: function(item) {
      // 深度拷贝
      item = JSON.parse(JSON.stringify(item));
      let _index = this.cartlist.map(x => x.id).indexOf(item.id);
      if (_index < 0) {
        this.cartlist.push({ ...item, numbers: 1 });
      } else {
        this.cartlist[_index].numbers += 1;
      }
      this.count+=1;
      this.$emit('addSuccess',this.count)
    }

同时,父组件加入isAdd判断,响应事件,触发回调函数。

代码语言:javascript
复制
		// shop.vue
		<cart ref="cart" @addSuccess="addSucessCallback"></cart>
		
		// ...
		add: function(item) {
        if(this.isAdd){
            this.$refs.cart.add(item)
        }else{
            alert('you can choose at most three items');
            return false;
        }
    },

    addSucessCallback:function(count){
        if(count>2){
            this.isAdd=false;
        } 
    }

当超过三个,就不许继续买了。

传参模式的选择:

子传父,最好就是派发事件。

父传子当然用props

规模较大时使用Vuex是最好的解决方案。


3. 其它api

动态样式

需求描述:取消勾选一个商品。设置样式为灰底。选中后消失。

代码语言:javascript
复制
<tr v-for="item in cartlist" :key="item.id" :style="{background:item.check?'':'#f5f5f5'}">
        // ...
        <td><input type="checkbox" v-model="item.check" @change="handleCheck($event,item)" /></td>
      </tr>
      
      
handleCheck:function(e,item){
        item.check=e.target.checked;
    },
计算属性

这个购物车中没有计算总价,要求每计算所有勾选的商品总价。

每次加购物车时,都默认选中:

代码语言:javascript
复制
this.cartlist.push({ ...item, numbers: 1,check:true });

现在把它实现了。在这里应用es6的reduce方法:

代码语言:javascript
复制
<tfoot v-if="cartlist.length>0" border=“1”>
        <td colspan="5" align="right">total</td>
        <td>${{total}}</td>
</tfoot>

//...
computed: {
      total() {
          let result=this.cartlist.reduce((sum,x)=>{          
              if(x.check){
                  console.log(parseInt(x.price)*x.numbers)
                  sum+=parseInt(x.price)*x.numbers;
              }
              return sum
          },0);
       
          return result;
      }
  },
监听:watch

假设我从sessionStorage中获取初始数据:

代码语言:javascript
复制
cartlist: JSON.parse(sessionStorage.cart)||[],

每次数据变动时,都更新sessionStorage。

这种操作非常麻烦,如果是这样,我得插几个眼?

监听数据cartlist变化,默认只看第一层,但如果我要监听第三层,就得加属性了。

代码语言:javascript
复制
watch: {
    cartlist: {
      deep: true,
      handler:function(newValue, oldValue) {
        console.log(newValue);
        sessionStorage.cart = JSON.stringify(newValue);
      }
    }
  },

自此。购物车所有功能顺利实现。


4. 组件库的使用:Element ui表单验证的使用和设计

element UI

Element UI的表单组件是一个很经典的表单实现。

实现代码如下:

代码语言:javascript
复制
<el-form ref="form" :model="form" label-width="80px">
  <el-form-item label="活动名称">
    <el-input v-model="form.name"></el-input>
  </el-form-item>
</el-form>

相信UI库的使用都没什么难度,这里主要关注这个表单组件的实现。

  • 数据模型(model,比如goods,price),
  • 校验规则(rules)是一个分字段的对象,比如:goods:[{required:true,message:'please type the goods'}]
  • form-item组件会带一个prop?用意何在?

思考如下问题:

el-form-item如何知道校验规则?表单全局校验是如何实现的 value绑定,input事件

设计form组件

接下来回到增加列表的表单中,继续造轮子。

把提交部分的表单独立为一个组件叫做Dform.vue。把相关方法数据都独立出来。

#####d-form-input

继续独立一个"d-form-input"组件。

实现双向绑定由2点决定:

  • 子组件通知父组件发生了input事件
  • 父组件响应事件
代码语言:javascript
复制
<template>
    <input :type="type" :value="value" @input="onInput($event)" />
</template>

<script>
export default {
  props: {
    type: {
      type: String,
      default: "text"
    },
    value:{
        type:String,
        default:''
    }
  },

  methods: {
      onInput(e) {
        // 通知父组件发生了input事件
        this.$emit('input',e.target.value)
      }
  },
};
</script>

<style scoped>
.input {
  display: block;
  width: 100%;
}
</style>

在dform组件中这么调用:

代码语言:javascript
复制
<d-input type="text" :value="model.goods" @input="model.goods=$event"/>
<!-- 等效于<d-input type="text" v-model="model.goods"/> -->

就实现了双向数据绑定!

d-form-item

d-form-item主要完成以下职责:

  • 接收一个label,当存在时,可以展示出来
  • 提供一个插槽(slot)存放可能的表单控件(input,button)

匿名插槽直接用<slot></slot>即可。具名插槽则需要这么写

代码语言:javascript
复制
// vue 2.6+

// 组件内
<slot name="foo"></slot>

// 使用时
<template v-slot:foo>
	foo content
</template>
  • 对输入过程中的内容进行校验。
代码语言:javascript
复制
<template>
  <div class="form-item">
    <div v-if="label" style="width:30%;text-align:right;">
      <span>{{label}}&nbsp;&nbsp;</span>
    </div>
    <!-- 插槽 -->
    <slot></slot>
    <!-- 校验信息 -->
    <div style="width:20%" v-if="errmsg">
        <span class="red">{{errmsg}}</span>
    </div>
    
  </div>
</template>

<script>
export default {
  props: {
    label: {
      type: String,
      default: ""
    }
  },
  data() {
      return {
          errmsg: ''
      }
  },
};
</script>

<style  scoped>
.form-item {
  display: flex;
  margin: 10px 20px;
}
.red{
    color: brown
}
</style>

有了这两个组件,那dform组件实际上写成这样:

代码语言:javascript
复制
<template>
  <div class="form" :model="model">

    <d-form-item label="goods">
      <d-input type="text" :value="model.goods" @input="model.goods=$event"/>
    </d-form-item>

    <d-form-item label="price">
      <d-input type="number" :value="model.price" @input="model.price=$event"/>
    </d-form-item>

    <d-form-item>
      <button style="display::block;width:90%;margin:auto;" @click="submit">confirm</button>
    </d-form-item>

  </div>
</template>
校验规则
继续重构

dform实际上是业务组件,和element ui相比,外层的form组件最好也应该封装重构。设想一个通用组件dd-form,应当具有的功能有:

  • 允许插槽存放。
  • 绑定model/rule。

d-form-item是最直接拿到表单校验的组件。拿取的方法:通过指定一个prop给它。

dform

代码语言:javascript
复制
<dd-form :model="model" :rules="rules" >
    <d-form-item label="goods" prop="goods">
      <d-input type="text" :value="model.goods" @input="model.goods=$event" />
    </d-form-item>

    <d-form-item label="price" prop="price">
      <d-input type="number" :value="model.price" @input="model.price=$event"/>
    </d-form-item>

    <d-form-item>
      <button style="display::block;width:90%;margin:auto;" @click="submit">confirm</button>
    </d-form-item>

  </dd-form>

dd-form要把校验规则传给formitem,可使用provide/inject方式。

代码语言:javascript
复制
<template>
  <div class="form" :model="model">
    <slot></slot>
  </div>
</template>

<script>

export default {
  // 类似data,provide可跨层级传递内容给子孙
  provide(){
    return {
      form:this //表单的实例可传递给后代
    }
  },
  props: {
    model:{
      type:Object,
      required:true
    },
    rules:{
        type:Object
    } //规则不需要额外指定
  },
  methods: {
   
  }
};
</script>

<style scoped>
.form {
  width: 60%;
  margin: 0 auto;
}

</style>

form-item:

代码语言:javascript
复制
//子组件中引入
  inject: {
    form: {
      default: () => {
        return {}
      }
    }
  },
d-input通知d-form-item发生了校验事件

很像jq的操作,可来实现向直系前一代祖先发送通知:

代码语言:javascript
复制
onInput(e) {
        // 通知父组件发生了input事件
        this.$emit('input',e.target.value);

        // 通知form-item做校验
        this.$parent.$emit('validate',e.target.value)
}
d-form-item响应校验事件

d-form-item用接收validate事件后,开启监听:

代码语言:javascript
复制
export default {
  props: {
    label: {
      type: String,
      default: ""
    }
  },
  created () {
      this.$on('validate',this.validate);
  },
  data() {
      return {
          errmsg: ''
      }
  },
  methods: {
      validate(e) {
      		// 不直接用e的原因是,不一定要出发onInput才校验,可能直接进行全局校验。
          console.log('执行校验:'+this.form.model[this.prop])
          // 获取父代发出的校验规则
          const descriptor={
              [this.prop]:this.form.rules[this.prop]
          }
      }
  },
};

运行程序,在每次输入时都会校验是否合理。

async-validator

Element ui 的校验库用的是async-validator 。它 是一个异步验证的库,需要传入要验证的数据和验证规则

官方链接 https://github.com/yiminghe/async-validator

你可以定义一个条件来对字段进行校验

代码语言:javascript
复制
      rules:{
        goods:[{required:true,message:'goods could not be null'}],
        price:[{required:true,message:'price could not be mull'}]
      }

现在就来安装运用这个库。

代码语言:javascript
复制
npm install async-validator 

引入

代码语言:javascript
复制
import Validator from 'async-validator';

在validate方法中,可以这样用

代码语言:javascript
复制
      validate(e) {
          console.log('执行校验:'+e);         
          // 获取校验规则,实际输出可能是{goods:{required:true,...}}
          const descriptor={
              [this.prop]:this.form.rules[this.prop]
          }
          //校验器
          const validator=new Validator(descriptor);
          let a=validator.validate({[this.prop]:this.form.model[this.prop]},err=>{
              if(err){
                  this.errmsg=err[0].message;
              }else{
                  console.log('校验成功')
              }
          });
      }

那么校验就实现了。为了未来全局操作的需要,validate需要设置一个返回值,成功为true,反之为false。

如前所述,async-validator是一个异步校验库。设置返回值需要用promise...resolve

代码语言:javascript
复制
	validate(e) {
      return new Promise(resolve => {
        console.log("执行校验:" + e);
        // 获取校验规则 实际输出可能是{goods:{required:true,...}}
        const descriptor = {
          [this.prop]: this.form.rules[this.prop]
        };
        //校验器
        const validator = new Validator(descriptor);
        validator.validate({ [this.prop]: e }, err => {
          if (err) {
            this.errmsg = err[0].message;
            resolve(false)
          } else {
            console.log("校验成功");
            this.errmsg
            resolve(true)
          }
        });

      });
    }
全局校验

凡事先搞清楚谁去做,做什么,什么时候做。

全局校验很明显,就是在提交时。操作的主体当然是dd-form(可通过this.refs.form).

业务逻辑:必须判断所有字段都通过校验。具体做法睡觉哦是对所有d-form-item进行循环校验。

问题来了,dd-form包含一个button,但button的父组件没有设置prop值因此不参与校验。判断依据在于,谁设置了prop,谁就需要校验。

在dd-form中定义校验方法

代码语言:javascript
复制
    async validate(callback) {
      //   执行表单所有校验,结果是由promise组成的数组。
      let tasks = this.$children.filter(x => x.prop).map(x => x.validate());
      // 接下来拿到的是由纯粹布尔值组成的数组。
      const results = await Promise.all(tasks);
      if (results.some(valid => !valid)) {
        callback(false);
      } else {
        callback(true);
      }
    }

在dform中

代码语言:javascript
复制
<dd-form :model="model" :rules="rules" ref="ddform">

    <d-form-item label="goods" prop="goods">
      <d-input type="text" :value="model.goods" @input="model.goods=$event" />
    </d-form-item>

    <d-form-item label="price" prop="price">
      <d-input type="number" :value="model.price" @input="model.price=$event"/>
    </d-form-item>

    <d-form-item>
      <button style="display::block;width:90%;margin:auto;" @click="submit('ddform')">confirm</button>
    </d-form-item>

  </dd-form>

最后对submit方法进行重构:

代码语言:javascript
复制
    submit: function(form) {
      this.$refs[form].validate(valid=>{
          if(valid){
            this.tableData.push({
            id: this.tableData.length + 1,
            name: this.model.goods,
            price: this.model.price,
            numbers: 1
          });
          this.model.goods = "";
          this.model.price = "";
        }else{
          alert('填完再提交');
          return false
        }
      })
      
    }

那么这个项目就终于,,做完了。


本节完。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-05-02,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 一Li小麦 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1. vue核心api:以购物车为例
    • 数据(插值)绑定:双大括号
      • 属性绑定(:xxx)
        • 指令
          • 用户输入(表单)
            • 请求数据的时机:created和mounted
              • 事件处理
                • 练习:完整实现购物车
                  • 添加商品列表
                  • 添加购物车
                  • 计算总价
              • 2. 组件化
                • 组件由重构开始
                  • 属性传参
                    • ref传参(不推荐但总是会用)
                      • 派发事件
                        • 总线模式
                        • 子组件传父组件
                      • 传参模式的选择:
                      • 3. 其它api
                        • 动态样式
                          • 计算属性
                            • 监听:watch
                            • 4. 组件库的使用:Element ui表单验证的使用和设计
                              • element UI
                                • 设计form组件
                                  • d-form-item
                                • 校验规则
                                  • 继续重构
                                  • d-input通知d-form-item发生了校验事件
                                  • d-form-item响应校验事件
                                  • async-validator
                                • 全局校验
                                领券
                                问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档