前言
随着重复项目的增多,通用组件在前端开发中越来越重要了,开发者更细分、聚焦于组件层面的开发,然后像搭积木一样完成应用功能。组件库可以统一管理组件,输出文档,能提升组件复用性、避免重复造轮子。
背景
为什么要搭建属于团队自己的组件库?
每个公司或者细化到团队都有属于自己的ui特性,例如(华为习惯用红色,平安习惯用橙色)。我们在利用开源的ui库的时候,免不了需要二次开发。正好团队最近在一些通用的机器人会话相关的产品,需要提供给集团下各个系列公司统一的样式、布局规范以接入,还需要统一扩展基础组件的能力。于是组件库的需求的就这么出来了!目前是以 vue2+webpack 的为基本框架来开发,后期我们将升级到vue3+vite的框架
。
1. 项目基础架构搭建
刚开始开发组件库,我们肯定是要建起项目的基础架构,既然是vue2 为基本框架,那自然是通过vue-cli 来构建项目了。
// 全局安装@vue/cli
npm install -g @vue/cli
// 创建基础项目,并选择vue2模板
vue create llz-ui
此时停笔思考:基础的vue脚手架搭建完成,接下来我应该干什么?我们的目的是要构建一个组件库,并且这个组件库需要提供给我们自己的前端工程项目引用,并且可以全局生效,此时脑海中大概有了一个思路:通过一个目录文件管理所有的ui组件,然后将这些组件统一提供一个出口,使之可以全局注册到前端项目中。这不就是我们平时引用第三方组件库时候的做法吗?OK,可以先看下我的项目目录结构:
llz-ui
├─ .browserslistrc
├─ .eslintignore
├─ .eslintrc.js
├─ .gitignore
├─ README.md
├─ babel.config.js
├─ components
│ ├─ button
│ │ ├─ button.vue
│ │ ├─ index.js
│ │ └─ index.less
│ ├─ icon
│ │ ├─ fonts
│ │ │ ├─ wbs-icon.svg
│ │ │ ├─ wbs-icon.ttf
│ │ │ ├─ wbs-icon.woff
│ │ │ └─ wbs-icon.woff2
│ │ ├─ icon.vue
│ │ ├─ index.js
│ │ └─ index.less
│ ├─ index.js
│ ├─ index.less
│ ├─ nav-bar
│ │ ├─ index.js
│ │ ├─ index.less
│ │ └─ nav-bar.vue
│ └─ style
│ ├─ base.less
│ ├─ hairline.less
│ ├─ theme.less
│ └─ utils.less
├─ demo
│ ├─ App.vue
│ ├─ assets
│ │ └─ logo.png
│ ├─ main.js
│ └─ modifiyStyle.less
├─ jsconfig.json
├─ package-lock.json
├─ package.json
├─ public
│ ├─ favicon.ico
│ └─ index.html
└─ vue.config.js
我们做了以下几个修改:
- 在根目录下新增了一个
components
目录,这个目录用来存放所有的即将开发的ui组件,例如button
、nav-bar
等等。 - 将
src
修改为demo
,其实此处修改意义不太大,但我的目的是让你更清楚的知道后面将通过demo里面的的业务代码验证引入组件后的效果,这里将相当于是一个验证的demo入口。
既然我们将 src 修改了名称,再启动项目必定是会报错,因为vue脚手架的默认入口是src路径下的。因此我们需要修改一下vue.config.js 的配置,相应的修改点如下:
- 修改 entry 入口配置;
- 配置路径别名,路径别名指向
components
的路径下; - 修改 css.loaderOptions,可以向webpack的预处理器loader 传递选项,本项目我们采用的less样式。
const { defineConfig } = require('@vue/cli-service')
const path = require("path");
module.exports = defineConfig({
outputDir: "dist",
publicPath: "./",
css: {
extract: true,
sourceMap: false,
loaderOptions: {
less: {
modifyVars: {
// 直接覆盖变量
// "color-primary": "orange",
// "color-success": "green",
// 或者可以通过 less 文件覆盖(文件路径为绝对路径)
hack: `true; @import "/demo/modifiyStyle.less";`,
},
},
},
},
pages: {
index: {
// page 的入口
entry: "demo/main.js",
// 模板来源
template: "public/index.html",
// 在 dist index.html 的输出
filename: "index.html",
// 当使用 title 选项时,
title: "Askbob Ui",
},
},
configureWebpack: {
// 路径配置
resolve: {
extensions: [".js", ".vue", ".jsx", ".css", ".less"],
alias: {
'@': path.resolve(__dirname, './components') // 路径别名
}
},
}
})
2. ui组件的开发 —— Button组件
我们知道需要在 components
目录下开发所有的ui组件,我们首先先实现一个简单的 button
组件。OK,那么我们新建一个 button 目录,并且在该目录下新建一个 button.vue
组件,我们在引用一个按钮组件的时候,是可以配置按钮的各种属性特征的,比如:按钮形状、按钮大小、是否禁用等等。业务代码中引用这个组件,再通过透传这些属性变量值,可以做到控制按钮的ui,那么我们就需要通过 props
来接受传入的属性值,这也是开发ui组件的最核心的地方: 通过 props
来接受变量控制当前组件的ui效果。话不多说,上代码:
// button.vue
<template>
<div
:disabled="disabled"
@click="handleClick"
:class="[
`askbob-button`,
`askbob-button--${size}`,
{
'is-disabled': disabled,
'is-round': round,
'is-border': border,
},
]"
>
<slot></slot>
</div>
</template>
<script>
export default {
name: `askbob-button`,
props: {
size: {
type:String,
default:'normal'
},
round: Boolean,
disabled: Boolean,
border:Boolean
},
methods: {
handleClick(event) {
if (!this.disabled) this.$emit("click", event);
},
},
};
</script>
上述代码中可以看到:
- 我们预留了一个插槽
<slot></slot>
,用来在引用组件的时候输入button的名称; - 通过
:class
中定义的不同类名来一起控制按钮的样式,askbob-button--${size}
接受size 参数,比如samll
、normal
等控制按钮大小,is-disabled
控制禁用效果,is-round
控制按钮的形状。
css 的代码如下。在如下代码中有几个细节:
- 头部通过
@import
引入一些公共样式; - css代码中有包括例如
@disabled-opacity
、@color-primary
的一些公共参数属性,这个也是定义在公共样式中的;
@import '../style/base.less';
@import '../style/theme.less';
@import '../style/utils.less';
.askbob-button {
position: relative;
width: 100%;
height: 36px;
background: @color-primary;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
.font-title2();
color: @color-white;
cursor: pointer;
&.is-disabled {
opacity: @disabled-opacity;
cursor: not-allowed;
&:active::after {
content: none !important;
}
}
&.is-round {
border-radius: 19px;
&:active::after {
border-radius: 19px;
}
}
&:active {
&::after {
content: '';
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
background: rgba(red(@color-active), green(@color-active), blue(@color-active), @active-opacity);
}
}
&--big {
height: 40px;
.font-title1();
font-weight: 600;
}
&--small {
height: 32px;
width: auto;
min-width:88px;
display: inline-block;
line-height: 32px;
padding: 0 @spacing-padding-base;
font-weight: 400;
}
&.is-border {
background: none;
border: 1px solid @color-primary;
color: @color-primary;
&:active {
background: @color-select-bg;
&::after {
content: none
}
}
&.is-disabled {
border: 1px solid #ccc;
color: #999999;
opacity: 1;
&:active {
background: none;
}
}
}
}
3. 组件的导出和全局注册
开发完一个简单的 button
组件之后,我们要面临下面的问题:我们开发的的 button 组件如何导出?如何能被项目引用并且生效?首先在button目录下新建index.js 文件,作用是为了导出 button 组件:
// index.js
import Button from './button';
export default Button;
在components
中,我们开发的组件除了button
以外会越来越多,这些组件汇总在components
目录中,我们需要提供一个公共的出口,使这些组件可以注册成功。我们在 components
根目录中新建一个公共出口文件 index.js
,通过动态导入的方式寻找 components
下所有的组件的路径,定义注册函数install(Vue)
,该方法可以将所有的组件通过全局注册的方式 Vue.component
依次注册成功。
// 通过require.context动态导入模块,且不需要显示导入
const components = [];
const routesContext = require.context("./", true, /index.js/);
routesContext.keys().forEach((modulePath) => {
const route = routesContext(modulePath);
if (route?.default?.name) {
components.push(route.default || route); // 获取路径
}
});
// 定义 install 方法注册组件
const install = function (Vue) {
if (install.installed) return;
install.installed = true;
// 遍历并注册全局组件
components.map((component) => {
Vue.component(component.name, component);
});
};
if (typeof window !== "undefined" && window.Vue) {
install(window.Vue);
}
export default {
install
};
4. 组件在demo中的使用
在demo项目中引用上述开发的组件,操作和正常的第三方ui组件库的饮用方式一样,在 main.js
引用:
import Vue from 'vue'
import App from './App.vue'
import AskbobUi from '@'
import '@/index.less'
Vue.use(AskbobUi)
Vue.config.productionTip = false
new Vue({
render: h => h(App),
}).$mount('#app')
在 App.vue
组件中使用:
<askbob-button size="small">
点我试试
</askbob-button>
发现是可以正常使用的,以此类推,其他的组件也采用类似的开发方式进行。