所谓UI组件库
,就是封装了平常项目开发中经常会使用的页面组件,发布至npm库中作为插件供项目组成员及其他开发者使用(不发布也行),目的就是为了避免多次重复劳动。
以插件的形式使用可以做到即插即用
,非常方便
。
市面上热门的UI组件库有Element-ui
(与Vue框架配合使用)、Ant Design
(与React框架配合使用)等等…
本文主要通过讲解Element-ui
封装一个组件的思路,来带领大家自己上手实践一下如何封装自定义组件
。最后也会讲述如何把成品发布至npm云端库来呈现我们的测试插件。
先上一张效果图:
实践之前,咱们先来瞅瞅Element-ui
组件库的项目结构,以及Button
组件的模块结构吧~
ELement-ui组件的源码的获取方式有两种:
npm install
来本地安装Element-ui
github上
拉取Element
团队开源的代码项目结构图如下:
lib文件夹下的内容是Element-ui
组件打包后的内容,是我们自己的项目使用Element-ui
时真正的引用出处。
packages
文件夹下包含了Element-ui
的所有组件源码,感谢走在开源前沿的Element前端团队
(听我说,谢谢你…)
src
文件夹主要用于测试packages
组件
types
文件夹下是typescript
的类型文件,咱们本次实践用不到,仅做了解
剩下一些文件CHANGELOG打头
的是不同语言版本的维护日志,LICENSE
是项目维护日志, package.json
是项目依赖及配置信息,README.md
是使用说明文档
我们定位到Element
文件夹下-package
文件夹下-button
组件,模块结构如下图所示:
其中button.vue
组件中是button
组件的具体实现:
按钮大小(size
)、类型(type
)、是否禁用(is-disabled
)、是否加载(is-loading
)、是否圆角(is-round
)、是否圆形按钮(is-circle
)都是通过props
属性,从父组件接收到的。
@click
事件通过emit
发送出去供父组件调用使用。
单独的disabled
属性是为了便于禁用绑定到button
上的事件,比如:点击事件。class
里的is-disabled
是为了给按钮添加禁用的样式。
script标签里的inject
是为了接收除直接父组件之外的其他更高级别父组件传递过来的属性,常与父组件中的provide
属性配合使用。
<template>
<button
class="el-button"
@click="handleClick"
:disabled="buttonDisabled || loading"
:autofocus="autofocus"
:type="nativeType"
:class="[
type ? 'el-button--' + type : '',
buttonSize ? 'el-button--' + buttonSize : '',
{
'is-disabled': buttonDisabled,
'is-loading': loading,
'is-plain': plain,
'is-round': round,
'is-circle': circle
}
]"
>
<i class="el-icon-loading" v-if="loading"></i>
<i :class="icon" v-if="icon && !loading"></i>
<span v-if="$slots.default"><slot></slot></span>
</button>
</template>
<script>
export default {
name: 'ElButton',
inject: {
elForm: {
default: ''
},
elFormItem: {
default: ''
}
},
props: {
type: {
type: String,
default: 'default'
},
size: String,
icon: {
type: String,
default: ''
},
nativeType: {
type: String,
default: 'button'
},
loading: Boolean,
disabled: Boolean,
plain: Boolean,
autofocus: Boolean,
round: Boolean,
circle: Boolean
},
computed: {
_elFormItemSize() {
return (this.elFormItem || {}).elFormItemSize;
},
buttonSize() {
return this.size || this._elFormItemSize || (this.$ELEMENT || {}).size;
},
buttonDisabled() {
return this.disabled || (this.elForm || {}).disabled;
}
},
methods: {
handleClick(evt) {
this.$emit('click', evt);
}
}
};
</script>
index.js
文件的主要功能是把封装好的button
组件给暴露出去:
关键点有install
属性和Vue.component
挂载。
import ElButton from './src/button';
/* istanbul ignore next */
ElButton.install = function(Vue) {
Vue.component(ElButton.name, ElButton);
};
export default ElButton;
细心的同学可能发现了,为啥封装button
组件的过程中没看到css
相关的代码呢?
这是因为Element
把所有组件的css
样式代码都统一放在了packages
文件夹下的theme-chalk
文件夹下。
感兴趣的同学可以了解一下,涉及到scss
预处理器的很多高级用法,鄙人能力目前还不能完全吃透:
以上就是对一个组件的封装,其实整个过程还是挺简单的。
组件的实现和平常写的业务代码基本一样。需要额外补充的地方就是需要一个index.js
文件把封装的组件通过install
属性和Vue.component
方法挂载到要使用的项目上。
一般UI组件库包含的组件那可不止一个呢,因此,还有一个全局的index.js
文件,就是用来把所有实现的组件汇总到一起进行挂载&&根据用户的需求来按需挂载。Element
把这个index.js
文件放在了src
文件夹下:
文件源码如下:
components
里存放Element
的所有组件,使用components.forEach
方法来依次把所有组件通过Vue.component
方法挂载到Vue
项目上。使用Vue.prototype
来挂载弹窗、加载框等不直接在页面上展示的组件。
当然,我们也可以不把所有组件都挂载在Vue
上,因为每个组件封装时也有一个index.js
文件,因此我们可以直接在src下的这个index.js
里直接把按需使用的组件使用export
进行导出。
/* Automatically generated by './build/bin/build-entry.js' */
import Pagination from '../packages/pagination/index.js';
...
import Button from '../packages/button/index.js';
import ButtonGroup from '../packages/button-group/index.js';
const components = [
...
Button,
ButtonGroup,
...
];
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: '2.15.6',
locale: locale.use,
i18n: locale.i18n,
install,
...
Button,
ButtonGroup,
...
};
了解了业界顶级的UI组件库实现一个组件的全过程后,咱们就可以自己上手来仿一个Element-ui
的Button
组件啦~
因为Element-ui
主要用于Vue2.X版本,因此咱们就用vue-cli来初始化一个Vue2.X的项目结构吧:
可以借助脚手架vue-cli,也可以用vite。本文采用的是vue-cli:
vue create sweet-ui-test
来初始化项目vue create sweet-ui-test
本次demo只需要Babel
和CSS预处理器
,因此只选中这两个就可以了。
(键盘上下键来切换选项,空格space键来切换选中)
vue中预处理器常用Less
,因此下面的选项中我们就选Less
即可
配置文件我们选择package.json
是否将本次偏好作为以后项目初始化的模板?我们键入N,表示不作为模板
静静等待项目初始化即可:
出现如下界面代表项目初始化完成:
在编辑器里打开项目,结构应该长这个样:
按照Element-ui
的风格来调整我们的项目结构:
package
文件夹packages
文件夹来放我们的组件代码src
文件夹的名称为examples
src
文件夹只是方便我们做测试用的。因此改个名来进行区分。vue.config.js
文件// vue.config.js
module.exports = {
pages: {
index: {
// 修改入口
entry: 'examples/main.js',
template: 'public/index.html',
filename: 'index.html'
}
},
chainWebpack: config => {
config.module
.rule('js')
.include
.add('/packages')
.end()
.use('babel')
.loader('babel-loader')
.tap(options => {
return options
})
}
}
exapmples
文件夹里不用的内容<template>
<div id="app">
app.vue
</div>
</template>
<script>
export default {
name: 'App',
}
</script>
在packages
文件夹下新建一个文件夹(名称自拟,本文中叫SweetButton
),用来存放我们的button
组件。
在SweetButton
下新建src
文件夹,在该文件夹下新增index.vue
文件。
在SweetButton
下新建index.js
文件,用来导出我们完成的button
组件。
在packages
文件夹下新建index.js
文件,用来导出所有组件。
上述几个步骤得到的packages
文件夹下的模块结构如下图所示:
接下来我们就来依次填充代码:
我们先来完善index.vue。仿照Element-ui
中button
组件的实现来完成我们的button
:
因为只是封装简单的button
组件,因此本次实践中使用less
把css
的代码写在了index.vue
里,没有另起一个文件来写。如果是准备封装一个功能完善的UI组件库的话,还是建议把css
部分抽离出去,不然像修改主题色这种需求就比较麻烦:
把完成的button
组件给暴露出去
import SweetButton from './src/index'
SweetButton.install = function (Vue){
Vue.component(SweetButton.name, SweetButton);
}
export default SweetButton;
import SweetButton from './SweetButton/index'
// 存放组件的数组
const components = [
SweetButton
]
// 定义 install 方法,接收 Vue 作为参数。
const install = function (Vue) {
// 判断是否安装
if (install.installed) return
// 遍历 components 数组,来进行全局注册
components.map(component => {
Vue.component(component.name, component)
})
}
export{
// 导出的对象必须具有 install,才能被 Vue.use() 方法安装
install,
SweetButton
}
至此,button
组件的封装就基本完成啦~
咱们去examples
文件夹下测试一下:
examples
下的main.js
里引入封装好的组件import Vue from 'vue'
import App from './App.vue'
import { SweetButton } from '../packages/index'
import '../lib/index.css'
Vue.config.productionTip = false
Vue.use(SweetButton)
new Vue({
render: h => h(App),
}).$mount('#app')
App.vue
中进行测试<template>
<div id="app">
<div class="first">
<sweet-button>主要按钮</sweet-button>
<sweet-button type="primary" @click="sayHai">主要按钮</sweet-button>
<sweet-button type="success">成功按钮</sweet-button>
<sweet-button type="info">信息按钮</sweet-button>
<sweet-button type="warning">警告按钮</sweet-button>
<sweet-button type="danger">危险按钮</sweet-button>
</div>
<div class="second">
<sweet-button round>主要按钮</sweet-button>
<sweet-button type="primary" round>主要按钮</sweet-button>
<sweet-button type="primary" round>主要按钮</sweet-button>
<sweet-button type="success" round>成功按钮</sweet-button>
<sweet-button type="info" round>信息按钮</sweet-button>
<sweet-button type="warning" round>警告按钮</sweet-button>
<sweet-button type="danger" round>危险按钮</sweet-button>
</div>
<div class="third">
<sweet-button disabled>主要按钮</sweet-button>
<sweet-button type="primary" disabled>主要按钮</sweet-button>
<sweet-button type="primary" disabled>主要按钮</sweet-button>
<sweet-button type="success" disabled>成功按钮</sweet-button>
<sweet-button type="info" disabled>信息按钮</sweet-button>
<sweet-button type="warning" disabled>警告按钮</sweet-button>
<sweet-button type="danger" disabled>危险按钮</sweet-button>
</div>
<div class="fourth">
<sweet-button>主要按钮</sweet-button>
<sweet-button type="primary" size="medium">主要按钮</sweet-button>
<sweet-button type="primary" size="small">主要按钮</sweet-button>
<sweet-button type="success" size="mini">成功按钮</sweet-button>
<sweet-button type="info" size="mini">信息按钮</sweet-button>
<sweet-button type="warning" size="small">警告按钮</sweet-button>
<sweet-button type="danger" size="medium">危险按钮</sweet-button>
</div>
</div>
</template>
<script>
export default {
name: 'App',
methods:{
sayHai(){
alert('希望你天天开心~')
}
}
}
</script>
<style lang="less">
div{
margin: 10px;
}
</style>
在3.3节中,我们实现了一个简易的button
组件,并在App.vue
中成功进行了测试。但上述测试只是通过项目内文件路径引用的方式来使用的组件,严格意义上来说,还需要进行打包,形成lib
文件才算是封装结束:
我们在package.json
文件夹下的scripts
脚本中新增一条脚本:
"lib": "vue-cli-service build --target lib --name index --dest lib packages/index.js"
用于把我们组件进行打包。之所以不用build
,是因为build
会把整个项目进行打包,而我们只想打包封装好的组件,因此需要自定义一条脚本。
运行npm run lib
, 静静等待一会儿…
此时项目结构树中,应该就会出现打包后的lib
文件夹:
我们修改examples
文件夹下的main.js
里SweetButton
文件的引入方式:
import Vue from 'vue'
import App from './App.vue'
import { SweetButton } from '../lib/index.umd.min'
// import { SweetButton } from '../packages/index'
import '../lib/index.css' // 加载样式文件
Vue.config.productionTip = false
Vue.use(SweetButton)
new Vue({
render: h => h(App),
}).$mount('#app')
重新运行项目,如果可以正常显示页面,说明打包成功!
此后,我们就可以在项目下任意一个.vue
文件里使用我们的自定义button
啦~
在第3节中,我们封装了一个仿element-ui
的button
组件,但目前这个组件库只能在我们当前的项目中使用,或者其他项目想使用的话需要拷贝lib
文件至他们的项目文件夹下,十分不方便…
那些大神封装的插件不都直接通过npm install
的方式进行导入的嘛,我也想自己封装的插件这么高级。
因此我们就需要把自己封装好的组件库发布至npm
中,作为插件方便其他开发者引用。
npm官网链接
name
是插件名称,必填npm
已有的名称冲突,建议先去npm
里搜一下,冲突的话会发布失败。version
是版本号,必填private
一定要为false
,必填private
默认为true
,记得修改,否则发布失败。执行npm publish
,出现类似下图界面就代表发版成功!success~
回到npm
官网中,输入我们的插件名,如果出现对应的插件,代表发版成功:
为了测试我们发版的插件,咱们需要新开一个Vue
的项目,不然在当前项目里执行npm install
时会提示和项目名称冲突。
项目初始化步骤就不重复了,初始化完成后,执行npm install xxx
(我们的插件名称),即可安装成功。
之后在main.js
里全局引用一下(引入步骤和其他UI组件库一样),或者在对应的组件里按需引入(引入步骤和其他UI组件库一样),即可方便的使用啦~
源码地址