所谓UI组件库,就是封装了平常项目开发中经常会使用的页面组件,发布至npm库中作为插件供项目组成员及其他开发者使用(不发布也行),目的就是为了避免多次重复劳动。
以插件的形式使用可以做到即插即用非常方便
市面上热门的UI组件库有Element-ui(与Vue框架配合使用)、Ant Design(与React框架配合使用)等等…
本文主要通过讲解Element-ui封装一个组件的思路,来带领大家自己上手实践一下如何封装自定义组件。最后也会讲述如何把成品发布至npm云端库来呈现我们的测试插件。
先上一张效果图:

实践之前,咱们先来瞅瞅Element-ui组件库的项目结构,以及Button组件的模块结构吧~
ELement-ui组件的源码的获取方式有两种:

  • 使用npm install来本地安装Element-ui
  • github上拉取Element团队开源的代码

1. Element-ui项目结构

项目结构图如下:

lib文件夹下的内容是Element-ui组件打包后的内容,是我们自己的项目使用Element-ui时真正的引用出处。

packages文件夹下包含了Element-ui的所有组件源码,感谢走在开源前沿的Element前端团队(听我说,谢谢你…)

src文件夹主要用于测试packages组件


types文件夹下是typescript的类型文件,咱们本次实践用不到,仅做了解


剩下一些文件CHANGELOG打头的是不同语言版本的维护日志,LICENSE是项目维护日志, package.json是项目依赖及配置信息,README.md是使用说明文档

2. Element的Button模块项目结构

我们定位到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><buttonclass="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,...
};

3. 上手封装一个简易的UI组件库

了解了业界顶级的UI组件库实现一个组件的全过程后,咱们就可以自己上手来仿一个Element-uiButton组件啦~

3.1 先上一张效果图


看上去是不是还挺像模像样滴~

3.2 项目初始化

因为Element-ui主要用于Vue2.X版本,因此咱们就用vue-cli来初始化一个Vue2.X的项目结构吧:
可以借助脚手架vue-cli,也可以用vite。本文采用的是vue-cli:

  • 执行vue create sweet-ui-test来初始化项目
vue create sweet-ui-test
  • 为了节省不必要的开支,这里采用手动配置


本次demo只需要BabelCSS预处理器,因此只选中这两个就可以了。
(键盘上下键来切换选项,空格space键来切换选中)

vue中预处理器常用Less,因此下面的选项中我们就选Less即可

配置文件我们选择package.json

是否将本次偏好作为以后项目初始化的模板?我们键入N,表示不作为模板

静静等待项目初始化即可:

出现如下界面代表项目初始化完成:

在编辑器里打开项目,结构应该长这个样:

3.2 项目结构调整

按照Element-ui的风格来调整我们的项目结构:

  • 新增package文件夹
    首先需要新增一个packages文件夹来放我们的组件代码
  • 修改src文件夹的名称为examples
    因为我们最终打包的只有package文件夹,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文件夹里不用的内容
    删去assets里的logo.png图片; 删去components文件夹。
    将examples的App.vue文件删除成下面这样:
<template><div id="app">app.vue</div>
</template><script>
export default {name: 'App',
}
</script>
  • 目前项目结构:
    目前项目结构应该长这样:
  • 测试项目能否正常跑通
    出现如下界面,就代表上述改造项目结构成功

3.3 上手编写button组件

packages文件夹下新建一个文件夹(名称自拟,本文中叫SweetButton),用来存放我们的button组件。
SweetButton下新建src文件夹,在该文件夹下新增index.vue文件。
SweetButton下新建index.js文件,用来导出我们完成的button组件。
packages文件夹下新建index.js文件,用来导出所有组件。
上述几个步骤得到的packages文件夹下的模块结构如下图所示:

接下来我们就来依次填充代码:

填充index.vue

我们先来完善index.vue。仿照Element-uibutton组件的实现来完成我们的button
因为只是封装简单的button组件,因此本次实践中使用lesscss的代码写在了index.vue里,没有另起一个文件来写。如果是准备封装一个功能完善的UI组件库的话,还是建议把css部分抽离出去,不然像修改主题色这种需求就比较麻烦:

<template><button :type="type"@click="handleClick"class="sweet-button":class="[`sweet-button--${type}`,`sweet-button--${size}`,{'is-disabled': disabled,'is-round': round,'is-circle': circle}]":disabled="disabled"><!--使用默认插槽来填充文本--><span v-if="$slots.default"><slot></slot></span></button>
</template><script>
export default {name: "sweetButton",inject: {},props: {type: {type: String,default: 'default'},size:{size: String,default: 'default'},disabled: Boolean,round: Boolean,circle: Boolean},methods:{handleClick(evt){this.$emit('click', evt);}}
}
</script><style scoped lang="less">
.sweet-button {padding: 8px 15px;border: none;color: white;font-size: 14px;border-radius: 4px;outline: none;transition: .1s;margin-left: 10px;cursor: pointer;
}.sweet-button--default {color: #606266;background-color: white;border: 1px solid #dcdfe6;
}.sweet-button--default:hover, .sweet-button--default:focus{color: #669fff;background-color: #ecf5ff;border: 1px solid #c9e3ff;
}.sweet-button--default.is-disabled {color: #c0c4cc;background-color: #fff;border-color: #ebeef5;cursor: not-allowed;
}.sweet-button--medium{font-size: 14px;padding: 6px 15px;
}.sweet-button--small{font-size: 12px;padding: 6px 15px;
}.sweet-button--mini{font-size: 12px;padding: 4px 13px;
}.sweet-button--primary {color: #fff;background-color: #409eff;border-color: #409eff;
}.sweet-button--primary:hover,.sweet-button--primary:focus {color: white;background-color: rgba(32, 160, 255, 0.7);
}.sweet-button--primary.is-disabled {color: #fff;background-color: #a0cfff;border-color: #a0cfff;cursor: not-allowed;
}.sweet-button--success {color: #fff;background-color: #67c23a;border-color: #67c23a;
}.sweet-button--success:hover,.sweet-button--success:focus {color: white;background-color: rgba(103, 194, 54, 0.7);
}.sweet-button--success.is-disabled {color: #fff;background-color: #b3e19d;border-color: #b3e19d;cursor: not-allowed;
}.sweet-button--info {color: #fff;background-color: #909399;border-color: #909399;
}.sweet-button--info:hover, .sweet-button--info:focus{color: white;background-color: rgba(144, 147, 153, 0.7);
}.sweet-button--info.is-disabled {color: #fff;background-color: #c8c9cc;border-color: #c8c9cc;cursor: not-allowed;
}.sweet-button--warning {color: #fff;background-color: #e6a23c;border-color: #e6a23c;
}.sweet-button--warning:hover, .sweet-button--warning:focus{color: white;background-color: rgba(230, 162, 60, 0.7);
}.sweet-button--warning.is-disabled {color: #fff;background-color: #f3d19e;border-color: #f3d19e;cursor: not-allowed;
}.sweet-button--danger {color: #fff;background-color: #f56c6c;border-color: #f56c6c;
}.sweet-button--danger:hover, .sweet-button--danger:focus {color: white;background-color: rgba(245, 108, 108, 0.7);
}.sweet-button--danger.is-disabled {color: #fff;background-color: #fab6b6;border-color: #fab6b6;cursor: not-allowed;
}.is-round{border-radius: 20px;
}
</style>
填充button组件的index.js

把完成的button组件给暴露出去

import SweetButton from './src/index'SweetButton.install = function (Vue){Vue.component(SweetButton.name, SweetButton);
}export default SweetButton;
填充packages文件夹下的index.js
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文件夹下测试一下:

测试封装好的button组件
  • 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>
  • 运行npm run serve
    如果页面上可以正常显示出这些样式各不相同的button的话,就证明成功~

3.4 打包组件

在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.jsSweetButton文件的引入方式:

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啦~

4. 发布组件库至npm

在第3节中,我们封装了一个仿element-uibutton组件,但目前这个组件库只能在我们当前的项目中使用,或者其他项目想使用的话需要拷贝lib文件至他们的项目文件夹下,十分不方便…
那些大神封装的插件不都直接通过npm install的方式进行导入的嘛,我也想自己封装的插件这么高级。
因此我们就需要把自己封装好的组件库发布至npm中,作为插件方便其他开发者引用。

4.1 注册成为npm的用户

npm官网链接

4.2 修改本地npm镜像源

4.3 为封装组件的项目添加npm用户信息

4.4 修改项目package.json文件信息

  • name是插件名称,必填
    插件名称不能和npm已有的名称冲突,建议先去npm里搜一下,冲突的话会发布失败。
  • version是版本号,必填
  • private一定要为false,必填
    private默认为true,记得修改,否则发布失败。

4.5 执行发布命令

执行npm publish,出现类似下图界面就代表发版成功!success~

回到npm官网中,输入我们的插件名,如果出现对应的插件,代表发版成功:

5. 插件测试

为了测试我们发版的插件,咱们需要新开一个Vue的项目,不然在当前项目里执行npm install时会提示和项目名称冲突。
项目初始化步骤就不重复了,初始化完成后,执行npm install xxx(我们的插件名称),即可安装成功。
之后在main.js里全局引用一下(引入步骤和其他UI组件库一样),或者在对应的组件里按需引入(引入步骤和其他UI组件库一样),即可方便的使用啦~
源码地址

【封装UI组件库】手把手教你仿一下Element-ui的Button组件(发布至npm)相关推荐

  1. pls-00302: 必须声明 组件_手把手教你开发vue组件库

    前言 Vue是一套用于构建用户界面的渐进式框架,目前有越来越多的开发者在学习和使用.在笔者写完 从0到1教你搭建前端团队的组件系统 之后很多朋友希望了解一下如何搭建基于vue的组件系统,所以作为这篇文 ...

  2. 手把手教你实现小程序中的自定义组件

    之前做小程序开发的时候,对于开发来说比较头疼的莫过于自定义组件了,当时官方对这方面的文档也只是寥寥几句,一笔带过而已,所以写起来真的是非常非常痛苦!! 好在微信小程序的库从 1.6.3 开始,官方对于 ...

  3. api 微信小程序组件库colorui_2020最全微信小程序UI组件库合集

    概述 " 这可能是2020年最全的UI组件集合了,希望对你有帮助,如果觉得好,别忘了给小编给点点赞鼓励! 微信小程序实用UI组件库合集 第一款 官方WeUI组件库 " 地址: ht ...

  4. 移动组件到指定坐标_手把手教你使用业界首创的象限图组件

    象限图经常用于规划事件的优先级,用于分析处理数据与期望的偏离程度等.目前各大主流的Angular组件库中均未发现类似象限图的组件,而DevUI组件库则基于DevCloud业务的诉求,设计并开发出一款灵 ...

  5. 手把手教你写一个微信小程序日历组件

    今天我们一起写一个微信小程序日历组件 微信小程序日历组件 github.com/749264345/w- 好,我们先看一下要实现的模样,如下图 由以上截图我们可以看到 1.日历可以通过按钮[切换展示效 ...

  6. Vue2.0进阶组件篇1 教你秒撸(短信倒计时组件)

    原本我想隔个几天再发文章,刚好今天项目上线,环境有问题,导致只有干等,刚好要为公司打造一套属于公司自己的一系列功能组件,这个使命就交给我了,大家也一直叫我来点干货,说实话我只是一个湿货,肚子里干一点就 ...

  7. android 微信创建群ui,Android控件:高仿微信主UI

    高仿微信主UI 之前在Android组件:Fragment切换后保存状态 一文中讲到了Fragment切换后,是如何保存原来的状态的,最重要的就是用add方法取代现在各种教程常见的replace方法. ...

  8. axure element ui素材_【Axure分享】基于Element UI的Axure Web组件

    有一段时间没做过产品原型了,前一阵有一个web产品需要做原型,正好在搞前端,于是顺便把Element UI移植到Axure上,基本上实现了大部分的功能组件,部分过于繁琐的组件未实现. 自己觉得蛮满意的 ...

  9. HeyUI组件库按需加载功能上线,盘点HeyUI组件库有哪些独特功能?

    HeyUI组件库 如果你还不了解heyui组件库,欢迎来我们的官网或者github参观. 官网 github 当然,如果能给我们一颗✨✨✨,那是最赞的了! 按需加载 当heyui组件库的组件越来越多的 ...

最新文章

  1. sonar jacoco 覆盖率为0_Jacoco统计代码覆盖率
  2. html可以导入MySQL吗_将数据从HTML文件(带有嵌入式JavaScript)导入MySQL数据库
  3. 搜索引擎-倒排索引基础知识
  4. RK3288 添加USB转虚拟串口设备
  5. javaWeb项目 IDEA中导入eclipes项目的方法。maven多模块项目(父子模块)与普通的web项目导入
  6. UIGestureRecognizer与UIButton Action在同一界面冲突的问题
  7. 从ngrx store里selector出来的Observable,执行subscribe的单步调试
  8. MYSQL安装时解决要输入current root password的方法
  9. 计组之存储系统:5、cache(cache功能、cache工作原理、cache性能分析)
  10. SAP云解决方案和企业本地部署(On-Premise)混合架构下的安全认证权限管理
  11. css之使用clearfix类清除浮动
  12. Linux关于DHCP详细的总结
  13. 洛谷——P1163 银行贷款
  14. 如何利用window下的Dos命令实现将多个txt合并成一个txt
  15. 《高等代数学》(姚慕生),习题1.2:三阶行列式
  16. 手机麦克风声音太大_手机麦克风没声音怎么设置?瞬间声音变大,一键设置即可...
  17. 徐思201771010132《面向对象程序设计(java)》第九周学习总结
  18. 基于springboot的员工管理系统整合Mybatis操作
  19. 计算机房的正常温度和湿度,机房适宜的湿度和温度是多少?
  20. 大类资产配置(一)均值方差模型MOV

热门文章

  1. 台积电赴美建厂成笑话?百亿补贴缩水10倍,创始人:我们低估了代价
  2. Android:关于天气预报图片序号99
  3. 逐浪软件智图->全网发布∞面向企业级的智能图库
  4. Python 类与对象的练习题
  5. 电脑持续蓝屏、掉盘看看是不是因为这个原因
  6. python是一种编译语言_Python是编译型语言还是解释型语言?
  7. 深度学习——L0、L1及L2范数
  8. PMBOK与CMMI,IPD,Scrum
  9. canvas drawImage() 方法
  10. 你的生命,我曾经如此焦灼过