组件以插件的形式引入使用,固然,也能够直接在页面引入组件文件,二者按需使用。html
安装插件:vue
import Button from './oyButton'; Button.install = function (Vue) { Vue.component(Button.name, Button); } export default Button;
vue.install源码:element-ui
export function initUse (Vue: GlobalAPI) { Vue.use = function (plugin: Function | Object) { # /*检测该插件是否已经被安装*/ if (plugin.installed) { return } const args = toArray(arguments, 1) args.unshift(this) if (typeof plugin.install === 'function') { # /*install执行插件安装*/ plugin.install.apply(plugin, args) } else if (typeof plugin === 'function') { plugin.apply(null, args) } plugin.installed = true return this } }
经过源码可知,vue不会重复安装同一个插件。以第一次安装为准api
如今,能够在代码中使用组件啦~数组
<oy-button>我是按钮按钮</oy-button>
以上,是一个很是简单的组件库实现。
如今来看看element组件库是如何实现的。浏览器
这里重点说下packages目录和src目录bash
|-- packages # 组件源码目录 |-- button # button组件目录,一个组件一个文件,方便管理 |-- src # 组件实现代码 |-- button-group.vue |-- button.vue |-- index.js # 组件入口文件 |-- src |--directives # 实现滚轮优化,鼠标点击优化 |--locale # 国际化 |--mixins # 公用逻辑代码 |--transitions # 样式过分效果 |--utils # 工具类包 |--index.js # 源码入口文件
整个目录结构很是清晰。app
button模块目录,有一个index.js做为模块入口异步
import ElButton from './src/button'; ElButton.install = function(Vue) { Vue.component(ElButton.name, ElButton); }; export default ElButton;
在index.js文件中,对组件进行拓展,添加Install方法。ide
import Button from '../packages/button/index.js'; const components = [Button] # 定义一个install方法 const install = function(Vue, opts = {}) { locale.use(opts.locale); locale.i18n(opts.i18n); # 将全部的功能模块进行注册。 components.map(component => { Vue.component(component.name, component); }); # 注册插件 Vue.use(Loading.directive); const ELEMENT = {}; ELEMENT.size = opts.size || ''; # 绑定Vue实例方法 Vue.prototype.$message = Message; }; if (typeof window !== 'undefined' && window.Vue) { install(window.Vue); } # 最后,将全部功能模块和install方法一块儿导出。 # 这样当引入element-ui时,即可以使用vue.use(element-ui)进行注册,即将全部的功能组件进行全局注册。 module.exports = { version: '2.3.8', locale: locale.use, i18n: locale.i18n, install, Button, } module.exports.default = module.exports;
element组件实现时,html基本实现了语义化标签。
标记组件。
Badge 标记组件部分源码:
<!-- sup标签语义:上标文本 --> <transition name="el-zoom-in-center"> <sup v-show="!hidden && (content || content === 0 || isDot)" v-text="content" class="el-badge__content" :class="{ 'is-fixed': $slots.default, 'is-dot': isDot }"> </sup> </transition>
ps: 本身写代码都是div span
element组件基本都兼容了v-model绑定值,组件使用起来更加温馨~
兼容v-model须要作一下几点:
(如text元素使用input事件来改变value属性 和 checkbox使用的change事件来改变check属性)
input组件源码:
export default { props: { # 定义value value: [String, Number], }, methods: { handleInput(event) { if (this.isOnComposition) return; const value = event.target.value; # 变动数据之后经过input去更新父组件数据 this.$emit('input', value); this.setCurrentValue(value); }, } }
vue中,存在几种组件之间数据传递的方案:
在平常开发中,父子组件之间数据传递用到比较多的方案是props。当组件层次比较深,就使用attrs来透传数据:
<el-select v-model="selectValue" v-bind="$attrs" v-on="$listeners"> <template v-if="label && keyValue"> <el-option v-for="(item, index) in selectList" :key="index" :label="item[label]" :value="item[keyValue]"></el-option> </template> </el-select>
element组件,在父子组件传递数据也是使用props,可是当组件层次比较深,或者不清楚组件层次时,使用的是:provide / inject
inject: { elForm: { default: '' }, elFormItem: { default: '' } },
关于provide / inject:
“这对选项须要一块儿使用,以容许一个祖先组件向其全部子孙后代注入一个依赖,不论组件层次有多深,并在起上下游关系成立的时间里始终生效” --vue文档简单来讲,就是父组件经过provide来提供变量,子组件经过inject来引用变量。
vue的inject源码:
# src/core/instance/inject.js export function initProvide (vm: Component) { const provide = vm.$options.provide if (provide) { vm._provided = typeof provide === 'function' ? provide.call(vm) : provide } }
provide是向下传递数据,先获取provide内容,而后传递给vm._provided设置成全局数据。inject会根据选项的 key 数组一层层向上遍历,拿到结果。
provide 相对于props,实现了跨层级提供数据。须要注意的是provide不是响应式的。
方法 | 解释 | 适用场景 |
---|---|---|
props | 用于接收来自父组件的数据 | 父子组件之间传递数据 |
provide | 以容许一个祖先组件向其全部子孙后代注入一个依赖,不论组件层次有多深,并在起上下游关系成立的时间里始终生效 | 替代嵌套过深的props,能够理解为一个bus,但只作父组件通知子组件的单向传递的一个属性 |
attrs | 包含了父做用域中不做为 prop 被识别 (且获取) 的特性绑定 (class 和 style 除外) | 父组件传向子组件传的,子组件没有经过prop接受的数据都会放在$attrs中 |
parent/child | 获取父/子组件实例 |
二者都是通知父组件执行事件的方法,可是有必定的区别:
对于组件嵌套过深,element本身实现了一个简易版的发布订阅方式:
function broadcast(componentName, eventName, params) { # 组件名称,事件名称,参数 # 当前组件下的子组件循环 this.$children.forEach(child => { # 获取组件名称 var name = child.$options.componentName; # 若是组件名称和要触发的事件组件名称相同 if (name === componentName) { # 当前子组件,调用$emit方法 child.$emit.apply(child, [eventName].concat(params)); } else { # 若是没有相等,那就继续查找当前子组件的子组件 broadcast.apply(child, [componentName, eventName].concat([params])); } }); }
export default { name: 'ElButton', props: { type: { type: String, default: 'default' }, }, };
组件在使用过程当中,会不断的优化添加功能,可是组件的内部变动不能影响组件的使用,这就须要组件有很好的扩展性,在一开始,可以提供足够比较友好的接口。
在组件中预留一些“插槽”,使用组件的时候,能够再“插槽”中注入自定义的内容,从而改变组件渲染结果。element组件库在这方面作得很好。
input组件部分源码:
<div> <template v-if="type !== 'textarea'"> <!-- 前置元素 --> <div class="el-input-group__prepend" v-if="$slots.prepend"> <slot name="prepend"></slot> </div> <input> <!-- 前置内容 --> <span class="el-input__prefix" v-if="$slots.prefix || prefixIcon" :style="prefixOffset"> <slot name="prefix"></slot> </span> <!-- 后置内容 --> <span> <span class="el-input__suffix-inner"> <template v-if="!showClear"> <slot name="suffix"></slot> </template> </span> </span> <!-- 后置元素 --> <div class="el-input-group__append" v-if="$slots.append"> <slot name="append"></slot> </div> </template> </div>
Input组件预留了四个“插槽”,容许使用者在先后位置均可以插入内容。
element组件提供了丰富的钩子函数:
focus() { (this.$refs.input || this.$refs.textarea).focus(); }, blur() { (this.$refs.input || this.$refs.textarea).blur(); },
组件要能接受必定的错误使用,能针对可预知的错误使用进行处理。
focus() { # 先判断this.$refs.input是否存在,才进行接下来操做,避免数据为空报错状况。 (this.$refs.input || this.$refs.textarea).focus(); }