上一章节中整体介绍了项目的工程化流程。接下来将开始深入分析组件源码系列学习,抽丝剥茧,学习各组件的逻辑实现。
首先看下组件源码涉及的目录结构,分析了解其机制。
目录结构
目录 packages
和 src
存放了组件源码、入口文件、各种公共辅助工具等。
📁packages
:组件源码(包含 主题样式 theme-chalk
)等;
📁src
:存放入口文件、自定义指令、国际化、混入方法、过渡效果、工具方法等。
├─packages # 存放组件源码 及主题(样式)
| ├─component-name # 组件源码
| ├─theme-chalk # 主题(样式)
├─src # 存放入口文件以及各种公共辅助工具
| ├─directives # 自定义指令(滚轮事件优化、鼠标点击优化)
| ├─locale # 国际化
| ├─mixins # mixin混入
| ├─transition # 过渡效果
| ├─utils # 工具方法
| ├─index.js # 组件库入口文件
接下来将对其逐一分析,耐心读完,相信会对您有所帮助!
📁 packages/component-name
组件功能的逻辑实现存放在 packages
目录下对应的同名目录下,组件名 component-name
(kebab-case
风格) 。
执行make
命令 make new <component-name> [中文名]
,调用脚本build/bin/new.js
自动生成组件基本代码,目录结构如下:
├─component-name # 组件目录,kebab-case 风格
| ├─index.js # 导出组件,同时定义其 install 方法
| ├─src # 组件源码目录
| | ├─main.vue # 组件功能逻辑实现
接下来以组件Avatar
示例,看下其 packages\avatar\index.js
代码实现:
// 引入组件
import Avatar from "./src/main.vue";
// 定义组件Avatar的 install 方法
Avatar.install = function (Vue) {
// 注册全局组件Avatar
Vue.component(Avatar.name, Avatar);
};
// 导出组件
export default Avatar;
index.js
主要是为组件扩展 install
方法。当在项目中按需引入 alert
功能组件时,可以使用vue.use(alert)
注册全局组件 Avatar
。
// 按需引入
import Vue from "vue";
import { Avatar } from "element-ui";
Vue.use(Avatar);
还有一些组件 Loading
、MessageBox
、Notification
、Message
使用方式与上述有所不同,将在后续文章中详细讨论。
📁 packages/theme-chalk 主题
packages/theme-chalk
目录存放各组件对应的 scss
文件、scss
相关变量、字体文件、mixin
及公共样式设置。 详情请阅读前文 工程化系列(五) 中“主题构建”章节,在此不再赘述。
📃 src/index.js
src/index.js
是组件库入口文件,引入所有组件、定义完整引入时注册组件的 install 方法,并导出组件库版本信息、国际化配置、install 和 各个组件(用于按需引入)。
执行脚本 build/bin/build-entry.js
,基于组件清单文件components.json
结合字符串模版库json-templater/string
自动生成。
完整引入
在引入 Element
时,传入一个对象用于语言设置、自定义 i18n 的处理方法、 组件的默认尺寸 size、弹框的初始 z-index 。
import Vue from "vue";
import Element from "element-ui";
import lang from "element-ui/lib/locale/lang/en";
Vue.use(Element, {
locale: lang,
size: "small",
zIndex: 3000,
i18n: function (path, options) {
// ...
},
});
new Vue({
el: "#app",
render: (h) => h(App),
});
按需引入
引入部分组件比如 Button
和
Select,可以使用两种方式进行注册:插件注册 Vue.use()
或 组件全局注册Vue.component()
;引入 locale
方法进行语言设置;将全局配置添加到Vue.prototype
上。
import Vue from "vue";
import { Button, Select } from "element-ui";
import lang from "element-ui/lib/locale/lang/en";
import locale from "element-ui/lib/locale";
// 设置语言
locale.use(lang);
// 全局配置
Vue.prototype.$ELEMENT = { size: "small", zIndex: 3000 };
// 引入组件
Vue.use(Button);
Vue.component(Select.name, Select);
new Vue({
el: "#app",
render: (h) => h(App),
});
📁 src/directives 自定义指令
📃 mousewheel.js
滚轮事件优化,解决不同浏览器、不同平台的兼容性问题。 用于 packages/table/src/table.vue
组件中的 v-mousewheel
指令。
📃 repeat-click.js
鼠标点击优化,当用户鼠标左键一直按住不松手,只会触发一次触发 mousedown 的回调。
用于 packages\input-number\src\input-number.vue
组件中的 v-repeat-click
指令。 在InputNumber
计数器点击 ➕、➖ 时会触发该指令。
用于 packages\date-picker\src\basic\time-spinner.vue
组件中的 v-repeat-click
指令。在TimePicker
时间选择器点击 🔺、🔻 时会触发该指令。
📁 src/mixins 混入方法
混入 (mixin) 提供了一种非常灵活的方式,来分发 Vue 组件中的可复用功能。
📃 emitter.js
broadcast
用于上层组件通知下层组件的事件广播,遍历当前组件的所有子组件,找到名称为 componentName 的子组件,然后调用其 $emit() 事件。
/**
* @param componentName 组件名称
* @param eventName 事件名称
* @param params 传递的参数;
*/
function broadcast(componentName, eventName, params) {
// 遍历所有子组件
this.$children.forEach((child) => {
var name = child.$options.componentName;
// 找到组件名为componentName的子组件,并调用该子组件的$emit方法;
// 否则,继续递归
if (name === componentName) {
child.$emit.apply(child, [eventName].concat(params));
} else {
broadcast.apply(child, [componentName, eventName].concat([params]));
}
});
}
dispatch
用于子组件发送消息给上层组件的组件通信,查找所有父级,直到找到名称为 componentName 的父组件, 然后调用其 $emit() 事件。
/**
* @param componentName 组件名称
* @param eventName 事件名称
* @param params 传递的参数;
*/
dispatch(componentName, eventName, params) {
// 当前父组件
var parent = this.$parent || this.$root;
// 当前父组件的组件名
var name = parent.$options.componentName;
// 通过$parent,一直向上找,直到组件名等于componentName
while (parent && (!name || name !== componentName)) {
parent = parent.$parent;
if (parent) {
name = parent.$options.componentName;
}
}
if (parent) {
// 如果找到目标组件,那么调用目标组件的$emit方法
parent.$emit.apply(parent, [eventName].concat(params));
}
}
📃 focus.js
指定组件获取焦点。
export default function (ref) {
return {
methods: {
focus() {
this.$refs[ref].focus(); // 获取焦点;
},
},
};
}
📃 locale.js
提供了 t
方法用于i18n
处理。
// t:i18n处理方法
import { t } from "element-ui/src/locale";
export default {
methods: {
t(...args) {
return t.apply(this, args);
},
},
};
在Select
组件源码中可以看到 t
函数是根据传入的 path 路径(el.select.loading
),从语言包中找到对应的文案。
import Locale from "element-ui/src/mixins/locale";
export default {
mixins: [Locale],
name: "ElSelect",
componentName: "ElSelect",
computed: {
emptyText() {
if (this.loading) {
return this.loadingText || this.t("el.select.loading");
}
// ...
},
},
// ...
};
src\locale\lang\zh-CN.js
可以查看到该组件的简体中文语言配置内容。el.select.loading
等同于 加载中
。
export default {
el: {
// ...
select: {
loading: "加载中",
noMatch: "无匹配数据",
noData: "无数据",
placeholder: "请选择",
},
// ...
},
};
📃 migrating.js
对组件发生改动的 props 或 eventName 给予提醒。
// 驼峰形式转成短横线连接的形式
import { kebabCase } from "element-ui/src/utils/util";
export default {
mounted() {
// 只在非生产环境下进行
if (process.env.NODE_ENV === "production") return;
// VNode 虚拟 DOM。Vue 通过建立一个虚拟 DOM 对真实 DOM 发生的变化保持追踪
if (!this.$vnode) return;
// 获取组件变更的Attributes Events 的名称和提醒文本
const { props = {}, events = {} } = this.getMigratingConfig();
// 获取当前页面组件使用的 Attributes 和 Events
const { data, componentOptions } = this.$vnode;
const definedProps = data.attrs || {};
const definedEvents = componentOptions.listeners || {};
// 判断当前使用Attributes Events,若在变更列表中,给予提醒。
for (let propName in definedProps) {
propName = kebabCase(propName); // compatible with camel case
if (props[propName]) {
console.warn(
`[Element Migrating][${this.$options.name}][Attribute]: ${props[propName]}`
);
}
}
for (let eventName in definedEvents) {
eventName = kebabCase(eventName); // compatible with camel case
if (events[eventName]) {
console.warn(
`[Element Migrating][${this.$options.name}][Event]: ${events[eventName]}`
);
}
}
},
methods: {
getMigratingConfig() {
return {
props: {},
events: {},
};
},
},
};
Input
组件中的引用。
import Migrating from "element-ui/src/mixins/migrating";
export default {
name: "ElInput",
mixins: [Migrating],
methods: {
getMigratingConfig() {
return {
props: {
icon: "icon is removed, use suffix-icon / prefix-icon instead.",
"on-icon-click": "on-icon-click is removed.",
},
events: {
click: "click is removed.",
},
};
},
},
};
页面组件使用 click
事件,加载后会有警告提醒 click is removed
。
📁 src/transition 过渡效果
collapse-transition.js
导出一个函数式组件,用于实现折叠展开过渡效果。在 Tree 树形控件
、Collapse 折叠面板
、NavMenu 导航菜单
组件中用于折叠展开效果。
函数式组件只处理状态和行为,将内容和行为分离,实现代码解耦,让其更容易复用。
// addClass:移除class类名方法 removeClass:移除class类名方法
import { addClass, removeClass } from "element-ui/src/utils/dom";
/**
* 定义组件 Transition 进入中、离开时的事件函数
* @class Transition
*/
class Transition {
// --------
// 进入中
// --------
beforeEnter(el) {
// ...
}
enter(el) {
// ...
}
afterEnter(el) {
// ...
}
// --------
// 离开时
// --------
beforeLeave(el) {
// ...
}
leave(el) {
// ...
}
afterLeave(el) {
// ...
}
}
// 导出函数式组件
export default {
name: "ElCollapseTransition",
functional: true,
/**
* 渲染函数
* @param {Function} h createElement的别名
* @param {String | Array} { children } children:VNode 子节点的数组
* @return {VNode} 创建虚拟节点 (virtual node)
*
*/
render(h, { children }) {
// 定义 on 事件修饰符
const data = {
on: new Transition(),
};
// 创建虚拟节点
return h("transition", data, children);
},
};
createElement
函数参数
// @returns {VNode}
createElement(
// {String | Object | Function}
// 一个 HTML 标签名、组件选项对象,或者
// resolve 了上述任何一种的一个 async 函数。必填项。
"div",
// {Object}
// 一个与模板中 attribute 对应的数据对象。可选。
{
// ...
},
// {String | Array}
// 子级虚拟节点 (VNodes),由 `createElement()` 构建而成,
// 也可以使用字符串来生成“文本虚拟节点”。可选。
[
"先写一些文字",
createElement("h1", "一则头条"),
createElement(MyComponent, {
props: {
someProp: "foobar",
},
}),
]
);
h('transition', data, children)
功能等同于 createElement
函数使用 transition
内置组件向子元素或子组件传递事件,创建新的虚拟节点。
createElement(
"transition",
// on 事件修饰符
{
on: {
beforeEnter(el) {
// ...
}
enter(el) {
// ...
}
afterEnter(el) {
// ...
}
beforeLeave(el) {
// ...
}
leave(el) {
// ...
}
afterLeave(el) {
// ...
}
}
},
[VNode]
);