【Vue进阶】青铜选手,如何自研一套UI库?

7,044 阅读9分钟

前言

更新

  1. 本地跑ange-ui项目的时候需要全局安装vuepress,目前vuepress有0.x和1.x版本(刚发布,查看1.x文档)系列,安装最新的vupress无法正常运行项目(感谢 @_呜啦啦啦火车笛 提出),本项目使用1.x vuepress会有以下问题:
  • 授权拒绝错误
  • 与最新版sas-loader(7.x)不兼容
  1. 使用0.x可以避免npm install vuepress@0.14.11 -g 几天前官方发布的vuepress 1.x版本:

谢谢大家支持!

即便是一个青铜,也要用王者的心去编码!

output

Github上关于Vue的UI库,大大小小不计其数,即便是已经被推广使用的成熟库,也有很多。很多时候,我们自研一套UI库,不是想要做得多牛逼,竞争过别人(事实咱也干不过人家,除非你不是一个人在战斗。毕竟这不仅是个技术活,还是个体力活),我们仅仅是源自一个青铜对王者的仰望或者是为满足内心的需求。

这里跟大家讲一个一步一步自研UI库的故事。

原文地址:github.com/qiud...

项目地址:Ange UI

开发一套UI库,做成不难(这里指的是半半半成品,全品也好难。。。),做好很难。但不慌,咱有秘籍:

  1. 一定的内功修为。所谓打铁还需自身硬,要做高复用组件的开发工作,对Vue.jsCSS3还是有一定的技术要求。那我要多牛逼才能写好这个UI库啊?这取决于你这套UI库的实现高度。
  2. 一些招式套路。藏经阁(Github)上有很多关于Vue的UI库:六脉神剑、独孤九剑应有尽有。人家怎么写的,咱们跟着来就好了。有人可能意见很大,那不是在模仿吗?这里要严正声明,我们不是在模仿,我们只是向标准靠拢。因为牛逼的就是标准的。

UI库的必要架构

一套成熟的开源UI库一般都具备以下几个特点:

  • 包含了组件源码
  • 完备的说明文档
  • 符合Eslint校验标准
  • 全面的单元测试
  • 完整的构建生态

因此,它们的目录架构也出现了主要的两种形式:

image
&
image
有点大同小异,这里我们按照第一种架构去开发。

  • build:存放构建配置文件
  • docs:官方说明文档
  • src:组件源码
  • test:单元测试用例(这里不作阐述)
  • eslintrc:基于eslint-plugin-vue的开发规范标准

搭建UI开发环境

观察社区几大UI库发现,它们都是基于webpack搭建了自己的构建配置,包括本地开发、生产环境构建、UI库的打包等,建立一套自己的构建生态。我们就不同了,业余一点(其实是善于应用开源工具),我们的docs文档是基于vuepress的,vuepress有自己一套的构建体系,所以我们只需要针对UI组件源码写一份打包配置就好了。

下面开始搭建打包配置(其实就是很久以前我们做的基于webpack的构建,现在cli用得多了,配置也不会写了,码耶): 首先在根目录建立configbuild文件夹,然后往两个目录分别新建文件,如下:

image

  • build-lib.js:node执行的脚本,读取并执行lib的配置进行构建;
  • webpack.base.conf.js:公共的构建配置;
  • webpack.lib.conf.js:针对UI组件打包的配置;
  • index.js:可变的配置信息;
  • prod.env.js:声明当前构建环境为生产环境的配置;

不是说只有一份打包的配置文件么?咋还多了那么多文件呢?是这样,虽然我们是业余的,但咱也想做得专业一点对吧(有利于对配置进行扩展管理)!

我们看 config/index.js有什么?

image
好像注释也很清楚?这些定义最终都被应用在 webpack.lib.conf.js中。

再看看build/webpack.base.conf.js里面的关键配置:

image
基础配置里面的entryoutput会被 webpack.lib.conf.js覆写;rules定义一系列loader的转换规则,其中eslint的校验就是在这里定义了一个eslint-loader

最后看一眼build/webpack.lib.conf.js配置:

image
lib里面重写entry的规则是,根据传参值(components)分别走不同的入口文件,一种只有一个./src/index.js,这个是UI对外注册的入口文件(这使得我们可以引入整个UI);另一种是各个UI组件的注册入口文件(这使得我们可以按需引入组件);

先给大家贴图直观感受下源码的目录结构:

image

那实际打包的时候走的是哪一种呢?真相是都走!在build-lib.js中,执行了三次打包,打包输出效果如下:

image
第一次的打包的输出是经过压缩的(压缩css样式表文件,可以看到输出文件带了min后缀),第二次是未压缩的,最后一次是对各组件分别打包。(PS:我很想给大家用红圈圈在图上标记下,奈何设备不允许。。。数度哽咽......Ubuntu系统下大家有好用的截图标记工具可以推荐下吗?)

开发一个组件

前面扎好了马步,终于到修炼招式的阶段了! 我们都知道,在应用某个插件的时候需要经过下面代码的调用:

import Ange from 'ange-ui'
Vue.use(Ange)

好奇下Vue.use在做什么处理呢?它其实就是注册/安装这个插件,根据use内部的定义,它通过调用install方法去注册插件,那么,Ange就必须是一个Function(会被use当做是install方法调用)或者是一个包含了install方法的Object

道理我都懂,可是install方法里面到底写些什么?

试想一下,我们希望在项目的任意位置都能引用这个插件,那我们的每一个组件是不是要在全局注册?比如通过下面这种方式全局注册组件:

Vue.component('pagination', pagination)

没错,install方法内部就是批量地全局注册组件。

搭好目录架构

首先我们按照下图的方式新建目录和文件:

image
src目录的index.js文件中定义install方法:

import './scss/ange.scss'  // 引入组件样式表,也可以让用户在使用的时候自行引入
import components from './components'

function install(Vue, opts = {}) {
    Object.values(components).forEach((each) => {
        Vue.component(each.name, each)
    })
}

if (typeof window !== 'undefined' && window.Vue) {
    install(window.Vue)
}
export default {
    version: '1.0.0',
    install,
    ...components
}

核心逻辑install就是对所有的组件循环注册在全局。components目录的 index.js 则是逐个对外暴露组件对象,其次每一个组件也有一个 index.js ,它的作用是为当前组件注入install方法。理所当然地,install里面是将该组件注册在全局,于是我们可以按需引用组件。

开发Button组件

组件开发的通用模板

<template>
    <component :is="'button'"></component>
</template>

<script>
export default {
    name: 'ag-button',
    props: {}
}
</script>

component是vue的内置组件,is参数设置成button,表明最终渲染的html是button标签,我们也可以直接使用button标签,但我们的按钮组件不一定是button,还可能是a标签,为了更好的拓展,这里使用component。

声明组件参数

export default {
    name: 'ag-button',
    props: {
        // 按钮类别
        primary: Boolean,
        secondary: Boolean,
        dashed: Boolean,
        link: Boolean,
        // 按钮状态
        color: {
            type: String,
            validator (val) {
                return new Set(['success', 'warn', 'danger']).has(val)
            }
        },
        // 按钮尺寸
        size: {
            type: String,
            validator (val) {
                return new Set(['large', 'normal', 'small']).has(val)
            }
        },
        // 图标按钮
        icon: String,
        // 圆形按钮(一般结合图标按钮使用)
        circle: Boolean,
        // 外链按钮
        external: Boolean,
        // 异步按钮
        loading: Boolean
    }
}

完善组件模板

<template>
    <component
        :is="tag"
        class="ange-btn"
        :class="[ btnSize, color, {
            'default': isDefault,
            'primary': primary,
            'secondary': secondary,
            'dashed': dashed,
            'link': link,
            'icon': icon,
            'circle': circle
        }]"
        @click="$emit('click', $event)"
        :disabled="loading">
        <span class="ange-btn-content">
            <!-- 图标按钮依赖 ag-icon 组件 -->
            <ag-icon
                v-if="icon"
                :icon="icon" />
            <slot />  <!-- 插槽接收按钮文本 -->
        </span>
    </component>
</template>

<script>
export default {
    // ...
    computed: {
        tag () {
            return this.external ? 'a' : 'button'
        },
        isDefault() {
            const type = [this.primary, this.secondary, this.dashed, this.link]
            return type.every((each) => !each)
        },
        btnSize() {
            return this.size || 'normal'
        }
    }
}
</script>

剩下的工作就是写好样式表了,你可以选择直接写在vue文件里面,也可以新建_scss/scss_样式表。

组件应用及效果在线查看

image

以上,便开发好一个button组件了,执行一下node build-lib.js或者npm run build:lib(现在package.json声明script)就可以打包这个UI框架,然后再将其发布到npm平台(如果你想...)

写好一份文档

在线查看 Ange UI Docs 写好文档是一个库不可或缺的部分,写的过程通过实际应用各组件,还可以对其进行测试校验。前面说到,我们的文档要基于vuepress开发,简洁的Markdown写法,很是方便。这里一篇 指南 可以很好地帮助你,它告诉了你如何搭建架构,写好配置以及部署上线,或者参考我这个 仓库 的配置。

假设docs/views/button.md是你的button组件的文档页面,要如何引用你的组件?

image
Ange UI内置引入了样式表,这里可以不用再引入,也可以按需只引入button组件:

import '@scss/ange.scss'
import Button from '@component/button'
Vue.use(Button)

至此,文档也就写好了!

最后的最后,按照 vuepress doc 部署到你的github仓库上就可以了!

本文通过button组件从0到1的开发,深入浅出阐述了Vue UI框架的开发流程,你对Vue.js的理解越深,组件的功能越复杂,你就会用到更多的高级用法。通过自研UI框架,我们也有很大的收获:

  • 重新温习webpack配置
  • 深入理解Vue内部机制,掌握更多的vue高级用法
  • 夯实js和css3基础
  • 掌握编程范式和设计模式

PS:CSS其实是UI开发中占比很重的部分,大家按照自己的风格组件化开发就好。给大家推荐几个很棒的配色网站:

最后,希望大家也能多多去尝试,这个青铜自研UI库的故事到这里就结束了。

The end.