乐趣区

跟着element学习写组件

如何使用 vue 写一个组件库
组件以插件的形式引入使用,当然,也可以直接在页面引入组件文件,两者按需使用。
安装插件:
import Button from ‘./oyButton’;
Button.install = function (Vue) {
Vue.component(Button.name, Button);
}
export default Button;
vue.install 源码:
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 不会重复安装同一个插件。以第一次安装为准
现在,可以在代码中使用组件啦~
<oy-button> 我是按钮按钮 </oy-button>
以上,是一个非常简单的组件库实现。现在来看看 element 组件库是如何实现的。
element 组件项目结构
这里重点说下 packages 目录和 src 目录
|– packages # 组件源码目录
|– button # button 组件目录,一个组件一个文件,方便管理
|– src # 组件实现代码
|– button-group.vue
|– button.vue
|– index.js # 组件入口文件
|– src
|–directives # 实现滚轮优化,鼠标点击优化
|–locale # 国际化
|–mixins # 公用逻辑代码
|–transitions # 样式过度效果
|–utils # 工具类包
|–index.js # 源码入口文件
整个目录结构非常清晰。
button 模块解析
button 模块目录,有一个 index.js 作为模块入口
import ElButton from ‘./src/button’;

ElButton.install = function(Vue) {
Vue.component(ElButton.name, ElButton);
};
export default ElButton;
在 index.js 文件中,对组件进行拓展,添加 Install 方法。
element 组件入口文件解析
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;
我写的组件与 elemnet 组件有什么不同
代码实现
1.html 语义化
element 组件实现时,html 基本实现了语义化标签。

这样在无 CSS 样子时也容易阅读,便于阅读维护和理解。
便于浏览器、搜索引擎解析。利于爬虫标记、利于 SEO

标记组件。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
2. 兼容 v-model
element 组件基本都兼容了 v -model 绑定值,组件使用起来更加舒适~ 兼容 v -model 需要做一下几点:

props 中要定义 value 属性。
数据变化后,通过事件触发父组件更新数据,同时传递变更后的值。

(如 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);
},
}
}
3. 组件之间传递数据
vue 中,存在几种组件之间数据传递的方案:

props
attrs
provide / inject
this.$parent/$this.$children

在日常开发中,父子组件之间数据传递用到比较多的方案是 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
获取父 / 子组件实例

4. 组件通信
emit/props 传递函数
两者都是通知父组件执行事件的方法,但是有一定的区别:

emit 执行的是异步方法,props 传递的函数在子组件中执行作为同步函数的形式执行的。
emit 无法返回函数结果,props 传递的函数可以返回函数结果。

发布订阅
对于组件嵌套过深,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]));
}
});
}
组件设计
1. 扁平化参数

传入的参数尽量设计简单点,避免复杂的对象。过于复杂的数据,在 watch 或者 update 的情况下,影响性能
扁平化的 props 也可以更好的更新数据,重置数据。其次,复杂的数据变更,外部可能会监听不到数据变化。
如果定义传入的传入数据是一个对象,那组件内部就要做大量的工作,来判断外部擦混入的对象的属性值是否正确,并找出需要的数据内容,增加了组件工作量,也不便组件的后续维护。

2. 良好的 api 接口设计

保持组件外部提供接口的精简,不要过于泛滥的提供接口。
组件可定制,如果常量变为 props 能应对更多的情况,那么就可以作为 props 从父组件引入。原有的常量可作为默认值。按钮组件的样式存在默认样式,但是可以通过 type 传入类型,定制 button 组件样式,使组件可以适用更多场景。

export default {
name: ‘ElButton’,

props: {
type: {
type: String,
default: ‘default’
},
},
};
3. 可扩展性
组件在使用过程中,会不断的优化添加功能,但是组件的内部变更不能影响组件的使用,这就需要组件有很好的扩展性,在一开始,能够提供足够比较友好的接口。
如何实现?
预留“锚点”
在组件中预留一些“插槽”,使用组件的时候,可以再“插槽”中注入自定义的内容,从而改变组件渲染结果。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();
},
4. 错误处理
组件要能接受一定的错误使用,能针对可预知的错误使用进行处理。

给 props 属性设置多个数据类型,同时保证传入和传出的数据类型相同。
如果组件中,某个字段是父组件一定要传入的,需要把 props 属性的 require 设置为 true。
给重要的 prop 属性设置默认数据。
兜底:数据展示或者使用父组件传入内容之前,要先判断数据是否存在。

focus() {
# 先判断 this.$refs.input 是否存在,才进行接下来操作,避免数据为空报错情况。
(this.$refs.input || this.$refs.textarea).focus();
}

退出移动版