最近看到一句话 我觉得说的非常正确:“感觉现在的业务开发,如果不是很特殊的需求,基本都能在对应的组件库内找到组件使用,这样编写代码就成了调用组件,但是却隐藏了组件内的思想,因此弱化了编程能力,所以我想写这么个分析系列来鞭策自己深入分析组件的原理,提高代码阅读理解能力,我觉得一定要记下点什么来,如果只是看不动笔感觉很快就忘了,因此准备持续写这么个分析。”
所以看了这篇博主超级索尼对Element源码的系列分析【还没有看完,理解了Layout(布局)和Button(按钮)】
一、插入篇:element-pro
组件库介绍
定制element
项目生成element-pro
业务组件库
- 项目构建基于[
elementUI
]源码结构,简化了项目,保留原始组件库构建功能。 - 在原有Vue UI组件的基础上进行封装或者完全独立开发,更加贴近实际业务逻辑。
- 默认在ElementUI组件上进行封装,同时支持引入其他Vue UI组件库。
- 它现在用于快速搭建后台CRUD功能页面,适用restful API场景页面,包括增加、删除、修改、更新操作。同时支持自定义操作,比如弹出对话框、展示错误提示等。
- 传送门:http://intranet.cn-beijing-6.element-pro.rjxaavlly3.elb.xiaomi.com/#/zh-CN/component/introduction
- 组件内部所有form表单,都基于@femessage/el-form-renderer渲染。所以使用前需要全局引入并注册组件
import ElFormRenderer from '@femessage/el-form-renderer';
Vue.component('el-form-renderer', ElFormRenderer);
- PS: 基于
element
的表单渲染器【详情:可查看 基于element的表单渲染器 (el-form-renderer) 这篇文章】
因为最近把需要把参与到的两个项目中可复用的组件,抽出来放在element-pro
这个库里,也是为了巩固我们的组件化开发以及封装组件库的思想。所以就入手了ElementUI源码阅读,我也觉得一定要记下点什么来,如果只是看不动笔感觉很快就忘啦呢,同时鞭策自己深入分析组件的原理,提高代码阅读理解能力。
二、正文:项目结构篇
众所周知,组件都在
packages
文件夹里面,这个文件夹下面,每一个文件夹对应一个组件,可以时不时的看个小组件。
build
:放置webpack的配置文件。examples
:放置element api的页面文档。packages
:放置element的组件(css样式放置在这个目录下的theme-chalk下)。src/directives
:放置自定义指令。src/locale
:放置语言的配置文件。src/mixins
:放置组件用的混合文件。src/transitions
:放置动画配置文件。src/utils
:放置用到工具函数文件。src/index.js
:组件注册的入口文件。test
:测试文件。types
:这个文件里放了typescript的数据类,还没找到哪里用了这里的类
├── build
│ ├── bin # js指向文件,大量文件操作
│ ├── md-loader # 自定义实现的md-loader,markdown语法解析loader
│ ├── config.js # 功能的webpack选项配置,`alias``externals`等
│ ├── webpack.xxx.js # webpack 配置文件
├── examples
│ ├── components # 文档站点搭建组件
│ ├── extension # element chrome插件
│ ├── i18n # 文档站点中用到国际化语言包
│ ├── pages # 文档站点中不同页面对应的vue组件,不同语言生成不同vue组件
│ ├── entry.js # 文档站点vue编译入口文件
│ ├── index.tpl # 文档站点的index.html 模板
│ ├── route.config.js # 文档站点的路由生成
│ ├── nav.config.json # 文档站点的路由配置文件
├── lib # 组件打包后的产物目录
├── packages # 每个组件对应一个文件,组件的具体实现(不包括样式)
│ ├── theme-chalk # 默认主题样式文件
│ ├── alert # alert组件源码
│ ├── ... # 其它组件源码
├── src # 一些基础的dom操作库,工具库,本地化,指令, mixins等实现
│ ├── index.js # 组件注册入口
│ ├── directives # 自定义指令
│ ├── locale # 内置国际化语言包及使用api
├── tests # 测试用例
├── Makefile # makefile
├── components.json # 所有组件列表
...
三、项目样式篇
packages
:放置element的组件(css样式放置在这个目录theme-chalk下)。
也众所周知,css样式放置packages
目录theme-chalk
下
其中,mixins
目录下的文件
config.scss
这个文件是源头,定义了一些命名空间等原始的值:
$namespace: 'el'; // 命名空间
$element-separator: '__'; // 元素之间的分隔符
$modifier-separator: '--'; // 修饰符分隔符
$state-prefix: 'is-'; // 状态前缀
mixin.scss
/* BEM
-------------------------- */
@mixin b($block) {
$B: $namespace+'-'+$block !global;
.#{$B} {
@content;
}
}
@mixin e($element) {
$E: $element !global;
$selector: &;
$currentSelector: "";
@each $unit in $element {
$currentSelector: #{$currentSelector + "." + $B + $element-separator + $unit + ","};
}
@if hitAllSpecialNestRule($selector) {
@at-root {
#{$selector} {
#{$currentSelector} {
@content;
}
}
}
} @else {
@at-root {
#{$currentSelector} {
@content;
}
}
}
}
@mixin m($modifier) {
$selector: &;
$currentSelector: "";
@each $unit in $modifier {
$currentSelector: #{$currentSelector + & + $modifier-separator + $unit + ","};
}
@at-root {
#{$currentSelector} {
@content;
}
}
}
b
方法为例
@mixin b($block) {
$B: $namespace+'-'+$block !global;
.#{$B} {
@content;
}
}
- !global 变量提升 将局部变量转换为全局变量,在其他函数体内也能访问到此变量
如果传入的 $block
是 alert
,那么解析后就是:
@mixin b($block) {
.el-alert {
@content;
}
}
也就是说b
的作用就是把传进来的组件名拼接成 el
命名空间的选择符。
#{$B}
是一种插值写法,最终解析出来的就是el-alert
#{}
插值,可以通过#{}
插值语法在选择器和属性名中使用SassScript
变量
when
@mixin when($state) {
@at-root {
&.#{$state-prefix + $state} {
@content;
}
}
}
这里是 @at-root
官网File: SASS_REFERENCE — Documentation by YARD 0.9.12
的解释: @at-root
指令可以将一条或者多条规则注册在文档的根目录,而不用限制在父选择器内。
- 所以:
@at-root
将父级选择器直接暴力的改成根选择器
那么这段翻译过来就是:
&.is-center {
@content;
}
/* 以 alert 举例:*/
@include when(center) {
justify-content: center;
}
/* 最终:*/
.el-alert.is-center {
justify-content: center;
}
所以总结一下,这个 when
的 mixin
就是给传入的类名字段加一个 is-前缀
…
还有类似m
、e
等
四、支持typescript
声明文件在types/*.d.ts
文件中,每个组件对应一个文件。
比如alert.d.ts
文件:
- 组件声明类都继承了
ElementUIComponent
类, ElementUIComponent
类继承了Vue, 所以每个组件都能通过ts编译时检查。
import { ElementUIComponent } from './component'
export type AlertType = 'success' | 'warning' | 'info' | 'error'
export type AlertEffect = 'dark' | 'light'
/** Alert Component */
export declare class ElAlert extends ElementUIComponent {
/** Title */
title: string
/** Component type */
type: AlertType
/** Descriptive text. Can also be passed with the default slot */
description: string
/** If closable or not */
closable: boolean
/** whether to center the text */
center: boolean
/** Customized close button text */
closeText: string
/** If a type icon is displayed */
showIcon: boolean
/** Choose effect */
effect: AlertEffect
}
五、组件单独打包
对应package.json里面的打包脚本:
"dist": "npm run clean && npm run build:file && npm run lint && webpack --config build/webpack.conf.js && webpack --config build/webpack.common.js && webpack --config build/webpack.component.js && npm run build:utils && npm run build:umd && npm run build:theme",
- 一长串的命令就是编译项目,生成目标文件。
- 只用关注里面打包组件的命令是:
webpack --config build/webpack.component.js
- 再看
build/webpack.component.js
配置文件:
const path = require('path');
const ProgressBarPlugin = require('progress-bar-webpack-plugin');
const VueLoaderPlugin = require('vue-loader/lib/plugin');
const Components = require('../components.json');
const config = require('./config');
const webpackConfig = {
mode: 'production',
entry: Components,
output: {
path: path.resolve(process.cwd(), './lib'),
publicPath: '/dist/',
filename: '[name].js',
chunkFilename: '[id].js',
libraryTarget: 'commonjs2'
},
...
};
module.exports = webpackConfig;
省略了配置中的loader
和plugins
,主要看这里的入口entry
,对应最外层的components.json
文件(这个文件很多地方都会用到)
// components.json
{
"pagination": "./packages/pagination/index.js",
"dialog": "./packages/dialog/index.js",
"autocomplete": "./packages/autocomplete/index.js",
"dropdown": "./packages/dropdown/index.js",
"dropdown-menu": "./packages/dropdown-menu/index.js",
"dropdown-item": "./packages/dropdown-item/index.js",
"menu": "./packages/menu/index.js",
...
}
- 里面的内容就是
{key: packageName, value: packageDir},
列出了所有的组件及路径。 - 再看
webpack.component.js
里面的output.filename: [name].js
,保留原组件名,实现了每个组件单独打包。
碎碎念
package.json
里面的打包脚本里面多多深入会发现很多东西,script脚本意义,暂且分析了element的项目架构,没有分析每个组件的源码实现