vue components 组件封装

原文链接:https://blog.csdn.net/qq_33988065/article/details/85124428

什么叫做组件化

vue.js 有两大法宝,一个是数据驱动,另一个就是组件化,那么问题来了,什么叫做组件化,为什么要组件化?接下来我就针对这两个问题一一解答,所谓组件化,就是把页面拆分成多个组件,每个组件依赖的 CSS、JS、模板、图片等资源放在一起开发和维护。 因为组件是资源独立的,所以组件在系统内部可复用,组件和组件之间可以嵌套,如果项目比较复杂,可以极大简化代码量,并且对后期的需求变更和维护也更加友好。

如何进行组件化开发
先看下图:

图片.png

这是 vue.js 中的一个报错,原因是使用了一个未经注册的组件 lx-xxx ,这个报错告诉我们一个道理:使用自定义组件之前必须注册
那么如何注册一个组件呢? Vue.js 提供了 2 种组件的注册方式,全局注册局部注册

  1. 全局注册

在 vue.js 中我们可以使用 Vue.component(tagName, options) 进行全局注册,例如

Vue.component('my-component', {
  // 选项
})
  1. 局部注册

Vue.js 也同样支持局部注册,我们可以在一个组件内部使用 components 选项做组件的局部注册,例如:

import HelloWorld from './components/HelloWorld'

export default {
  components: {
    HelloWorld
  }
}

区别:全局组件是挂载在 Vue.options.components 下,而局部组件是挂载在 vm.$options.components 下,这也是全局注册的组件能被任意使用的原因。
组件化开发必备知识

所谓工欲善其事,必先利其器,在正式开发一个组件之前,我们先要掌握一些必备的知识,这里我只会简单介绍一下,详情参考官网。

name

组件的名称,必填

<lx-niu/>
<lx-niu></lx-niu/>

name: 'lxNiu'

js 中使用驼峰式命令,HTML 使用kebab-case命名。

props

组件属性,用于父子组件通信,可通过this.msg访问

<div>{{msg}}</div>

props: {
  msg: {
    type: String,
    default: ''
  }
}

show: Boolean // 默认false

msg: [String, Boolean]  // 多种类型

computed

处理data或者props中的属性,并返回一个新属性

<div>{{newMsg}}</div>

computed: {
  newMsg() {
    return 'hello ' + this.msg
  }
},

注:因为props,data和computed在编译阶段都会作为vm的属性合并,所以不可重名

render

用render函数描述template

<lx-niu tag='button'>hello world</lx-niu>

<script type="text/javascript">
  export default {
    name: 'lxNiu',
    props: {
      tag: {
        type: String,
        default: 'div'
      },
    },
    // h: createElement
    render(h) {
      return h(this.tag,
        {class: 'demo'}, 
        this.$slots.default)
    }
  }
</script>

render 中的 h 其实就是 createElement,它接受三个参数,返回一个 vnode
h 参数解释:
args1: {string | Function | Object} 用于提供DOM的html内容
args2: {Object} 设置DOM样式、属性、绑定事件之类
args3: {array} 用于设置分发的内容

注:vue编译顺序: template–> compile --> render --> vnode --> patch --> DOM

slot

分发内容,有传入时显示,无传入 DOM 时显示默认,分为无名和具名两种,this. solts.default默 认 指 向 无 名 插 槽 , 多 个 s l o t 时 用 法 this. solts.default默认指向无名插槽,多个slot时用法this.slots.name

<lx-niu>
  <div slot='header'>header</div>
  <div class="body" slot='body'>
    <input type="text">
  </div>
  <div slot='footer'>footer</div>

  <button class='btn'>button</button>
</lx-niu>

<template>
  <div>
    <slot name='header'></slot>
    <slot name='body'></slot>
    <slot name='footer'></slot>
    <slot></slot>
  </div>
</template>

<script>
  export default {
    name: 'lxNiu',
    mounted() {
      this.$slots.header // 包含了slot="foo"的内容
      this.$slots.default // 得到一个vnode,没有被包含在具名插槽中的节点,这里是button
    }
  }
</script>

class

定义子组件的类名

// 父组件
<lx-niu round type='big'/>

// 子组件
<div :class="[
  type ? 'lx-niu__' + type : '',
  {'is-round': round},
]">控制</div>

//真实DOM
<div class='lx-niu__big is-round'>hello</div>

其他属性

$attrs

v-bind="$attrs" 将除class和style外的属性添加到父组件上,如定义input:

<input v-bind="$attrs">

v-once

组件只渲染一次,后面即使数据发生变化也不会重新渲染,比如例子中val不会变成456

<template>
  <div>
    <button @click="show = !show">button</button>
    <button @click="val = '456'">button</button>
    <div v-once v-if="show">
      <span>{{val}}</span>
    </div>
  </div>
</template>

<script>
export default {
  data() {
    return{
      show: false,
      val: '123'
    }
  },
};
</script>

mixins

// mixin.js
export default {
  data() {
    return{
       msg: 'hello world'
    }
  },
  methods: {
    clickBtn() {
      console.log(this.msg)
    }
  },
}

// index.vue
<button @click="clickBtn">button</button>

import actionMixin from "./mixin.js";
export default {
  methods: {},
  mixins: [actionMixin]
}

实例演示

比如我们要注册一个 lx-button 这样一个组件,那么目录和伪代码如下:


图片.png

index.vue

<template>
  <button>lxButton</button>
</template>

<script>
export default {
  name: 'lxButton'
}
</script>

index.js

import lxButton from './src/index'

lxButton.install = (Vue) => {
  Vue.component(lxButton.name, lxButton)
}

export default lxButton

其中 install 是 Vue.js 提供了一个公开方法,这个方法的第一个参数是 Vue 构造器,第二个参数是一个可选的选项对象。
MyPlugin.install = function (Vue, options){}

watch-弹窗实现原理

<button @click="dialogVisible = true">显示</button>
<lx-niu :visible.sync="dialogVisible"></lx-niu>

<script>
  export default {
    data() {
      return {
        dialogVisible: false
      }
    },
    watch: {
      dialogVisible(val) {
        console.log('father change', val)
      }
    }
  }
</script>

定义组件

<template>
  <div v-show="visible">
    <button @click="hide">关闭</button>
  </div>
</template>

<script>
  export default {
    name: 'lxNiu',
    props: {
      visible: Boolean
    },
    watch: {
      visible(val) {
        console.log('child change:', val)
      }
    },
    methods: {
      hide() {
        this.$emit('update:visible', false);
      }
    },
  }
</script>

点击父组件中的显示按钮,改变传入子组件中的值,点击子组件中的关闭,改变父组件中值。

注:@click=“dialogVisible = true” 点击时将dialogVisible的值改为true
注::visible.sync: 双向数据绑定,配合update:visible使用,实现子组件修改父组件中的值

col组件实例

export default {
  name: 'ElCol',

  props: {
    span: {
      type: Number,
      default: 24
    },
    tag: {
      type: String,
      default: 'div'
    },
    offset: Number,
    pull: Number,
    push: Number,
    xs: [Number, Object],
    sm: [Number, Object],
    md: [Number, Object],
    lg: [Number, Object],
    xl: [Number, Object]
  },

  computed: {
    gutter() {
      let parent = this.$parent;
      while (parent && parent.$options.componentName !== 'ElRow') {
        parent = parent.$parent;
      }
      return parent ? parent.gutter : 0;
    }
  },
  render(h) {
    let classList = [];
    let style = {};

    if (this.gutter) {
      style.paddingLeft = this.gutter / 2 + 'px';
      style.paddingRight = style.paddingLeft;
    }

    ['span', 'offset', 'pull', 'push'].forEach(prop => {
      if (this[prop] || this[prop] === 0) {
        classList.push(
          prop !== 'span'
            ? `el-col-${prop}-${this[prop]}`
            : `el-col-${this[prop]}`
        );
      }
    });

    ['xs', 'sm', 'md', 'lg', 'xl'].forEach(size => {
      if (typeof this[size] === 'number') {
        classList.push(`el-col-${size}-${this[size]}`);
      } else if (typeof this[size] === 'object') {
        let props = this[size];
        Object.keys(props).forEach(prop => {
          classList.push(
            prop !== 'span'
              ? `el-col-${size}-${prop}-${props[prop]}`
              : `el-col-${size}-${props[prop]}`
          );
        });
      }
    });

    return h(this.tag, {
      class: ['el-col', classList],
      style
    }, this.$slots.default);
  }
};

col组件使用render函数,而不是template来实现组件,原因有两个:

1.该组件有大量的类判断,如果采用template代码比较冗余,使用js代码更加简洁
2.直接render描述性能更好

button组件实例

<template>
  <button
    class="el-button"
    @click="handleClick"
    :disabled="buttonDisabled || loading"
    :autofocus="autofocus"
    :type="nativeType"
    :class="[
      type ? 'el-button--' + type : '',
      buttonSize ? 'el-button--' + buttonSize : '',
      {
        'is-disabled': buttonDisabled,
        'is-loading': loading,
        'is-plain': plain,
        'is-round': round,
        'is-circle': circle
      }
    ]"
  >
    <i class="el-icon-loading" v-if="loading"></i>
    <i :class="icon" v-if="icon && !loading"></i>
    <span v-if="$slots.default"><slot></slot></span>
  </button>
</template>
<script>
  export default {
    name: 'ElButton',

    inject: {
      elForm: {
        default: ''
      },
      elFormItem: {
        default: ''
      }
    },

    props: {
      type: {
        type: String,
        default: 'default'
      },
      size: String,
      icon: {
        type: String,
        default: ''
      },
      nativeType: {
        type: String,
        default: 'button'
      },
      loading: Boolean,
      disabled: Boolean,
      plain: Boolean,
      autofocus: Boolean,
      round: Boolean,
      circle: Boolean
    },

    computed: {
      _elFormItemSize() {
        return (this.elFormItem || {}).elFormItemSize;
      },
      buttonSize() {
        return this.size || this._elFormItemSize || (this.$ELEMENT || {}).size;
      },
      buttonDisabled() {
        return this.disabled || (this.elForm || {}).disabled;
      }
    },

    methods: {
      handleClick(evt) {
        this.$emit('click', evt);
      }
    }
  };
</script>

icon组件

1.组件封装


图片.png
<template>
  <svg
    :class="getClassName"
    :width="width"
    :height="height"
    aria-hidden="true">
    <use :xlink:href="getName"></use>
  </svg>
</template>

<script>
  export default {
    name: 'icon-svg',
    props: {
      name: {
        type: String,
        required: true
      },
      className: {
        type: String
      },
      width: {
        type: String
      },
      height: {
        type: String
      }
    },
    computed: {
      getName () {
        return `#icon-${this.name}`
      },
      getClassName () {
        return [
          'icon-svg',
          `icon-svg__${this.name}`,
          this.className && /\S/.test(this.className) ? `${this.className}` : ''
        ]
      }
    }
  }
</script>

<style>
  .icon-svg {
    width: 1em;
    height: 1em;
    fill: currentColor;
    overflow: hidden;
  }
</style>

2.全局注册使用


图片.png

svg文件夹下是静态svg图片


图片.png
/**
 * 字体图标, 统一使用SVG Sprite矢量图标(http://www.iconfont.cn/)
 *
 * 使用:
 *  1. 在阿里矢量图标站创建一个项目, 并添加图标(这一步非必须, 创建方便项目图标管理)
 *  2-1. 添加icon, 选中新增的icon图标, 复制代码 -> 下载 -> SVG下载 -> 粘贴代码(重命名)
 *  2-2. 添加icons, 下载图标库对应[iconfont.js]文件, 替换项目[./iconfont.js]文件
 *  3. 组件模版中使用 [<icon-svg name="canyin"></icon-svg>]
 *
 * 注意:
 *  1. 通过2-2 添加icons, getNameList方法无法返回对应数据
 */
import Vue from 'vue'
import IconSvg from '@/components/icon-svg'
import './iconfont.js'
//全局注入
Vue.component('IconSvg', IconSvg)

const svgFiles = require.context('./svg', true, /\.svg$/)
const iconList = svgFiles.keys().map(item => svgFiles(item))

export default {
  // 获取图标icon-(*).svg名称列表, 例如[shouye, xitong, zhedie, ...]
  getNameList () {
    return iconList.map(item => item.default.id.replace('icon-', ''))
  }
}

在main.js 中引入

图片.png

3.在页面中使用
name的值,为svg下面的静态svg图片名称

   <icon-svg name="zhedie"></icon-svg>
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,193评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,306评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,130评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,110评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,118评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,085评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,007评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,844评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,283评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,508评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,667评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,395评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,985评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,630评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,797评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,653评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,553评论 2 352

推荐阅读更多精彩内容