“ 本文正在参加「金石计划 . 瓜分6万现金大奖」”
一 源码文件分析
1 Element在vue中的本质
这是我们在使用element时在main.js中引入的。这是vue引入新组件的方式。
可以得出结论:Element-ui最终的形式是一个外部的新vue组件,可添加集成到Vue主组件中,element-ui这个组件内部也包含着很多小的Vue组件,例如Element经常使用的Button等组件。
Element既能全局引入,也能单独引入需要的组件,其本质就是因为,整个element-UI是一个大的vue组件,每个小的功能组件也是一个单独的vue组件。
2 源码查看方式
1 github克隆代码:element-ui源码链接。
2 本地项目中查看:在使用过Element-ui 项目中,打开node_modules,找到element文件夹。
3 目录解读
这是我22.11.19在github中下载的element-ui 2.15.12版本代码文件,结构如下:
在分析代码时,主要分析的是一下五个文件/文件夹
补充:
locala 语言包,不同地区使用不同,会在src/index.js中使用。
transitions:放置动画配置的文件
补充:
- .travis.yml:持续集成(CI)的配置文件,它的作用就是在代码提交时,根据该文件执行对应脚本,成熟的开源项目必备之一。
- CHANGELOG:更新日志,土豪的 ElementUI 准备了 4 个不同语言版本的更新日志。
- components.json:配置文件,标注了组件的文件路径,方便 webpack 打包时获取组件的文件路径。
- element_logo.svg:ElementUI 的图标,使用了 svg 格式,合理使用 svg 文件,可以大大减少图片大小。
- FAQ.md:ElementUI 开发者对常见问题的解答。
- LICENSE:开源许可证,ElementUI 使用的是 MIT 协议,使用 ElementUI 进行二次开发的开发者建议注意该文件。
- Makefile:在 .github 文件夹下的贡献指南中提到过,组件开发规范中的第一条:通过 make new 创建组件目录结构,包含测试代码、入口文件、文档。其中 make new 就是 make 命令中的一种。make 命令是一个工程化编译工具,而 Makefile 定义了一系列的规则来制定文件变异操作,常常使用 Linux 的同学应该不会对 Makefile 感到陌生。
下面我们就来逐步分析element源码的五个主要文件/文件夹
4 主入口文件index.js
./src/index.js
这个文件继承了所有elemen-ui的组件,为原型提供了一些方法,也是webpack打包的入口文件
/* Automatically generated by './build/bin/build-entry.js' */
import Pagination from '../packages/pagination/index.js';
// ...
// 引入组件
const components = [
Pagination,
Dialog,
// ...
// 组件名称
];
const install = function(Vue, opts = {}) {
// 国际化配置
locale.use(opts.locale);
locale.i18n(opts.i18n);
// 批量全局注册组件
components.forEach(component => {
Vue.component(component.name, component);
});
// 全局注册指令 使用vue.use方法的时候,会自动调用install函数,,所以只需要在install函数中
//批量全局注册各种指令,组件,挂载全局方法。
Vue.use(InfiniteScroll);
Vue.use(Loading.directive);
// 全局设置尺寸
Vue.prototype.$ELEMENT = {
size: opts.size || '',
zIndex: opts.zIndex || 2000
};
// 在 Vue 原型上挂载方法
Vue.prototype.$loading = Loading.service;
Vue.prototype.$msgbox = MessageBox;
Vue.prototype.$alert = MessageBox.alert;
Vue.prototype.$confirm = MessageBox.confirm;
Vue.prototype.$prompt = MessageBox.prompt;
Vue.prototype.$notify = Notification;
Vue.prototype.$message = Message;
};
/* istanbul ignore if */
if (typeof window !== 'undefined' && window.Vue) {
install(window.Vue);
}
export default {
version: '2.9.1',
locale: locale.use,
i18n: locale.i18n,
install,
CollapseTransition,
// 导出组件
};
代码已经注释的很清楚,需要注意的是第一句话:
/* Automatically generated by './build/bin/build-entry.js' */
含义:该文件是由 build/bin/build-entry.js 生成的
需要重点注意的我都加了注释:
var Components = require('../../components.json');
var fs = require('fs');
var render = require('json-templater/string');
var uppercamelcase = require('uppercamelcase');
var path = require('path');
var endOfLine = require('os').EOL;
// 输出地址
var OUTPUT_PATH = path.join(__dirname, '../../src/index.js');
// 导入模板
var IMPORT_TEMPLATE = 'import {{name}} from \'../packages/{{package}}/index.js\';';
// 安装组件模板
var INSTALL_COMPONENT_TEMPLATE = ' {{name}}';
// 模板
var MAIN_TEMPLATE = `/* Automatically generated by './build/bin/build-entry.js' */
{{include}}
import locale from 'element-ui/src/locale';
import CollapseTransition from 'element-ui/src/transitions/collapse-transition';
const components = [
{{install}},
CollapseTransition
];
const install = function(Vue, opts = {}) {
locale.use(opts.locale);
locale.i18n(opts.i18n);
components.forEach(component => {
Vue.component(component.name, component);
});
Vue.use(InfiniteScroll);
Vue.use(Loading.directive);
Vue.prototype.$ELEMENT = {
size: opts.size || '',
zIndex: opts.zIndex || 2000
};
Vue.prototype.$loading = Loading.service;
Vue.prototype.$msgbox = MessageBox;
Vue.prototype.$alert = MessageBox.alert;
Vue.prototype.$confirm = MessageBox.confirm;
Vue.prototype.$prompt = MessageBox.prompt;
Vue.prototype.$notify = Notification;
Vue.prototype.$message = Message;
};
/* istanbul ignore if */
if (typeof window !== 'undefined' && window.Vue) {
install(window.Vue);
}
export default {
version: '{{version}}',
locale: locale.use,
i18n: locale.i18n,
install,
CollapseTransition,
Loading,
{{list}}
};
`;
delete Components.font;
var ComponentNames = Object.keys(Components);
var includeComponentTemplate = [];
var installTemplate = [];
var listTemplate = [];
// 根据 components.json 文件批量生成模板所需的参数
ComponentNames.forEach(name => {
var componentName = uppercamelcase(name);
includeComponentTemplate.push(render(IMPORT_TEMPLATE, {
name: componentName,
package: name
}));
if (['Loading', 'MessageBox', 'Notification', 'Message', 'InfiniteScroll'].indexOf(componentName) === -1) {
installTemplate.push(render(INSTALL_COMPONENT_TEMPLATE, {
name: componentName,
component: name
}));
}
if (componentName !== 'Loading') listTemplate.push(` ${componentName}`);
});
// 传入模板参数
var template = render(MAIN_TEMPLATE, {
include: includeComponentTemplate.join(endOfLine),
install: installTemplate.join(',' + endOfLine),
version: process.env.VERSION || require('../../package.json').version,
list: listTemplate.join(',' + endOfLine)
});
// 生成入口文件
fs.writeFileSync(OUTPUT_PATH, template);
console.log('[build entry] DONE:', OUTPUT_PATH);
此文件使用了json-templater 来生成了入口文件
它通过引入 components.json 这个我们前面提到过的静态文件,批量生成了组件引入、注册的代码。
优点:我们不再需要每添加或删除一个组件,就在入口文件中进行多处修改,使用自动化生成入口文件之后,我们只需要修改一处即可。
5 交互操作优化方法directives
directives文件夹中只有两个js文件,分别是:
mousewheel.js :滚轮事件优化
repeat_click.js:单击事件优化
代码也比较简短 就直接在代码中进行注释说明
import normalizeWheel from 'normalize-wheel';
const isFirefox = typeof navigator !== 'undefined' && navigator.userAgent.toLowerCase().indexOf('firefox') > -1;
const mousewheel = function(element, callback) {
if (element && element.addEventListener) {
element.addEventListener(isFirefox ? 'DOMMouseScroll' : 'mousewheel', function(event) {
const normalized = normalizeWheel(event);
callback && callback.apply(this, [event, normalized]);
});
}
};
export default {
bind(el, binding) {
mousewheel(el, binding.value);
}
};
百度后得知:mousewheel.js只是一个jQuery中的一个鼠标滚轮插件,该文件的主要作用是:
对鼠标滚动事件进行了优化,使用normalize-wheel这个库来解决不同浏览器之间的兼容性来获取x方向和y方向的滚动偏移量。
import { once, on } from '@/utils/dom';
export default {
bind(el, binding, vnode) {
let interval = null;
let startTime;
// 获取指令绑定的事件函数
const handler = () => vnode.context[binding.expression].apply();
// 定义一个清除定时器的函数
const clear = () => {
// 间隔时间小于100毫秒时,继续执行回调函数
if (Date.now() - startTime < 100) {
handler();
}
clearInterval(interval);
interval = null;
};
// 添加点击事件
on(el, 'mousedown', (e) => {
if (e.button !== 0) return;
// 缓存点击时的时间
startTime = Date.now();
// 添加鼠标弹起的事件
once(document, 'mouseup', clear);
// 清除定时器
clearInterval(interval);
// 100毫秒执行一次回调函数
interval = setInterval(handler, 100);
});
}
};
该文件就是一个用于优化单击事件的指令,我们平时点击时,正常的点击逻辑是这样的:
当用户按住鼠标左键时,会触发mousedown的回调。但当一直按住鼠标左键不松手时,就不会触发mousedown的回调,
使用该指令就是为了实现一直按住鼠标左键不松手时,也能执行对应的事件,这指令主要用在InputNumber组件中,当鼠标点击-或+不松开时,数字可以持续的进行加减。
6 mxins外部方法
此文件下共有三个js文件:
先上总结:
在mxins外部混入的方法中,一共有以下几个导出的方法:
dispatch方法:子组件发送消息给上层组件。
boradcast方法:上层组件通知,
focus方法:聚焦
getMigratingConfig方法:非生产环境下判断props和events命名规范(不允许驼峰)
(1)emitter.js
源码及注释
function broadcast(componentName, eventName, params) {
this.$children.forEach(child => {
var name = child.$options.componentName;
if (name === componentName) {
child.$emit.apply(child, [eventName].concat(params));
} else {
broadcast.apply(child, [componentName, eventName].concat([params]));
}
});
}
export default {
methods: {
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));
}
},
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和broadcast,特别是dispatch似曾相识,那必须要一探究竟了
dispatch方法:子组件发送消息给上层组件。
boradcast方法:上层组件通知下层组件。
(2)focus.js
export default function(ref) {
return {
methods: {
focus() {
this.$refs[ref].focus();
}
}
};
};
使用focus方法在注册过refs的集合中获取ref对象,并聚焦。
.focus()事件 将用户的光标自动放到焦点上去,无需用户进行单独的操作
(3)locale.js
import { t } from 'element-ui/src/locale';
export default {
methods: {
t(...args) {
return t.apply(this, args);
}
}
// 国际化工具配置,没啥好讲的
(4) migrating.js
对发生改动的props或eventName发出警告
import { kebabCase } from 'element-ui/src/utils/util';
export default {
mounted() {
if (process.env.NODE_ENV === 'production') return;
if (!this.$vnode) return;
const { props = {}, events = {} } = this.getMigratingConfig();
const { data, componentOptions } = this.$vnode;
const definedProps = data.attrs || {};
const definedEvents = componentOptions.listeners || {};
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: {}
};
}
}
};
代码也比较简单:开始引入了一个kebabCase函数,在until.js中找到该函数功能如下:
export const kebabCase = function(str) {
const hyphenateRE = /([^-])([A-Z])/g;
return str
.replace(hyphenateRE, '$1-$2')
.replace(hyphenateRE, '$1-$2')
.toLowerCase();
};
kebabCase函数是把驼峰的命名(abAb)改为下划线连接ab_ab的形式, 在非生产环境中如果出现就会报错警告
7 utils 工具方法集合
utils文件夹中放置了许多组建中通用的函数方法,这里就不一一展开,后期在组件源码的学习中在看。
写结几个常见的:
dom.js:事件监听、事件注销、一次执行、是否有XXX class 、增加class、移除某class、获取style、设置style、是否滚动、获取滚动的节点、某元素是否包含在某容器里。
types.js:事件监听、事件注销、一次执行、是否有XXX class 、增加class、移除某class、获取style、设置style、是否滚动、获取滚动的节点、某元素是否包含在某容器里。
8 packags.json
每个项目这个文件夹都是中点,可以在里面找到我们想要的很多解决方案
对于element-ui中的配置文件,需要关注的是以下几个重点:
1 files:
files是对外发布的内容:lib是打包后生成的文件目录
dist
命令展开后如下(后面为注释):
"dist": "
npm run clean && //删除上次打包生成的文件及目录
npm run build:file &&
npm run lint &&// 利用eslint。根据.eslint和.eslintignore文件,检测代码规范
webpack --config build/webpack.conf.js &&
webpack --config build/webpack.common.js &&
webpack --config build/webpack.component.js &&
npm run build:utils && //设置BABEL_ENV=utils
npm run build:umd && //利用babel-core及插件add-module-exports和transform-es2015-modules-umd处理src/locale/lang下的文件,生成umd格式的文件;
npm run build:theme//构建主题
"
npm run build:file:
利用postcss,根据package/theme-chalk/src/icon.scss,往example目录生成icon相关的信息;
利用json-templater/string模板引擎,根据根目录下components.json,往src目录下生成index.js文件,index.js主要是引入packages目录下的组件及install(vue插件)方法,并对外export;
利用正则,根据examples/i18n/page.json和examples/pages/template,生成不同语言的文件,examples的内容相当于element UI官网。
webpack --config build/webpack.conf.js:
入口文件:src/index.js(npm run build:file生成)
输出:以umd形式输出到lib/index.js;
loader:babel-loader处理jsx等文件;vue-loader处理packages下面的vue组件
webpack --config build/webpack.common.js:
入口文件:src/index.js(npm run build:file生成)
输出:以commonjs2形式输出到lib/element-ui.common.js;
loader:babel-loader处理jsx、babel和es6等文件;vue-loader处理packages下面的vue组件;style-loader和css-loader处理css文件;以url-loader处理图片等;
**webpack --config build/webpack.component.js:
**入口文件:components.json,包含packages下的组件;
输出:把packages下的组件,以commonjs2形式分别输出到lib目录;
loader:babel-loader处理jsx、babel和es6等文件;vue-loader处理packages下面的vue组件;style-loader和css-loader处理css文件;以url-loader处理图片等;
dev
"npm run bootstrap && // 启动bootstrap
npm run build:file && //前面讲过
cross-env NODE_ENV=development webpack-dev-server --config build/webpack.demo.js &
node build/bin/template.js",//开发环境下启动webpack打包
test
通过karma测试工具和mocha, sinon-chai测试框架进行单元测试
pub
"pub": "
npm run bootstrap &&
sh build/git-release.sh &&
sh build/release.sh &&
node build/bin/gen-indices.js &&
sh build/deploy-faas.sh
"
- npm run bootstrap:
安装依赖,注意的是vue是以peerDependencies的形式配置的 - sh build/git-release.sh:
git checkout dev - sh build/release.sh:
a.checkout master分支,并合并dev分支;
b.通过npx临时安装select-version-cli,与开发者进行交互,更新版本信息;
c.执行npm run dist;
d.测试ssr;
e.进入packages/theme-chalk目录,利用npm version和npm publish,发布主题(packages/theme-chalk是个基于gulp的工程),由此可见elementUI的主题是可以独立发布的,不过会保证version跟elementUI保持一致;
f.退回到根目录,提交代码并通过npm version更新版本(更新package.json中的版本号);
g.在当前分支(a步骤切换到master)push代码,然后checkout dev分支,并rebase master分支,最后push代码;
h.如果version为beta,则通过npm publis --tag打上标签,否则直接publish - node build/bin/gen-indices.js
利用algoliasearch进行搜索,需要把examples/docs/下的.md文件内容以一定格式上传给algolia
参考资料: