Vue UI组件库开发经验漫谈

UI组件是对一组相关的交互和样式的封装,提供了简单的调用方式和接口,让开发者能很便捷地使用组件提供的功能来实现业务需求。

我在一个名为Admin UIVue UI组件库(GitHub地址:https://github.com/BboyAwey/admin-ui)上投入了大量时间。该库到目前版本发展到3.x,几乎全部出自我手。它的优劣请先搁置不问,但至少,我从中积累和学习到的经验足够回答一个问题:如何开发一个Vue 组件库。不过即使你是React的使用者,也可以参考本文给出的经验,因为如果你打算编写一个React UI组件库,你将不得不面对几乎完全一样的问题。

这篇文章也是我在公司的一次技术分享的内容。我在这里主要只探讨思路,尽量不去涉及具体实现。并且我对这些问题的解决思路也不尽然是完全合理的,如有错漏请读者斧正。

1 组织你的项目

当你开始着手组件库的开发时,第一件事可能就是建立一个项目,因为是Vue 组件库,你很可能会使用其官方推荐的vue-cli工具来生成一个项目。

1.1 合适的文件结构

当项目生成后,你很快就发现这个项目模版的文件结构用于业务开发非常合适,但并不那么适合组件库的开发。这时你很可能会在其src文件夹下用你的组件库名称建立一个文件夹来存放你的组件库代码。但这个时候你还并不清楚所有需要做的事情,你并没有继续调整文件结构。

我们暂且将你的组件库就命名为admin-ui,方便后续行文

当你真正开始编写第一个组件时,你肯定会首先编写一个用来展示正在开发中的组件的页面,并在其上对其进行测试。所以你又在src文件夹中新建了一个examples文件夹用来存放你的示例代码。

这时你的文件结构看起来就像这样:

组件库文件夹和示例页面文件夹

1.2 为组件库编写或生成使用文档

很可能一段时间后,你为每个组件都编写了一个示例页,甚至其中一些示例页本身已经做的很棒了。如果有一天组件库主要开发完成了,这堆实例页也就没什么用了。于是你可能会对这些示例页进行完善,将每个组件的特性和接口列表甚至使用示例代码放到它们的示例页上,然后将其部署在一台服务器上方便你的用户随时查看。它们很幸运地没有被浪费,而是都变成了组件库的使用文档了!

1.3 组件库本身开发文档的管理

然后你可能意识到一个人的力量之薄弱,你会邀请其它开发者参与到你的项目中,然而尽管你在使用vue-cli生成项目时已经开启了ESlint,但多人协同开发一套完整的UI库仅仅依靠代码风格的统一是远远不够的。你们可能需要建立开发文档,将各种约定和设计,以及需要共享的其它信息发布在其中。所以你又在项目的根目录中新建了一个documentation目录,并在其中使用GitBook生成了一个文档,同时同步到了GitBook服务器,以便你的伙伴们即使没有同步它们也能在线查看。

使用gitbook建立的组件开发者文档

1.4 各种安装方式的支持

你现在已经可以开始开发你的组件了,在编写第一个组件的时候,你意识到你现在编写的这个项目本质上是你的组件库的使用文档,如果这算是一个业务项目,那么它是直接将你的组件库源码放置到了自己的源码中来使用。但如果其它业务项目组使用了你的组件库,他们目前只能像你现在这样把源码拷贝到他们的项目中使用,并且每次你的组件库升级了,他们都需要通过再次进行拷贝来进行升级,这种情况显然是你所不能接受的。你开始考虑你的用户们怎么安装你的组件库了。

你首先想到的是最流行的安装方式:npm。如果直接将你的组件库发布到npm上,你的用户将能通过它或者yarn非常方便地进行安装和升级。但目前你的组件库刚刚起步,你并不希望马上开源。于是你向公司申请了一个你们公司自己搭建的gitlab中的仓库,然后在你的组件库所在的那个文件夹git init 初始化了一个git项目,并将其同步到你申请的那个仓库中。这时,公司的同事们已经可以通过

npm install admin-ui git+ssh://admin-ui-git-location.git --save

来安装你的组件库了。

然后你能想到的第二种安装方式就是CDN。你的用户们通过在他们的页面内联

<script src="admin/ui/cdn/location"></script>

来使用你的组件库,这时就涉及到如何打包你的组件库了。在这种场景下,你需要将你的组件库打包为一整个admin-ui.js这样的js文件来使用。关于打包我们将在下一节继续讨论。

当然,最后一种安装方式就是直接使用源码了,将你的组件库直接放到项目源码中进行引用。

1.5 打包和发布你的组件库

确定了需要支持的安装方式,你可能已经意识到,现在你的项目中有两部分需要打包和发布:

  • 你的组件库(这是重点)
  • 你的组件库的使用文档(也就是项目本身)

你首先想到的是使用文档打包起来很方便,因为这个项目目前的打包配置直接就能将你编写的所有示例页面打包好,你唯一要做的就是运行

npm run build

但关键问题在于你的组件库admin-ui。它目前是作为你这个项目的一部分源码存在的。所以你不得不开始思考如何对这部分代码进行单独打包。

当然,你也可以不对你的组件库进行打包,而是直接将其源码发布为一个npm包,但那样的话,用户在使用它的时候就需要依赖打包工具来对你的代码进行打包。而类似vue-cli这类工具生成的项目,默认情况下是不会打包来自node_modules文件夹下的代码,用户必须修改构建配置手动指定需要打包的代码位置,这很不方便

于是你在对照着build.jswebpack.prod.conf.jsprod.env.js,在项目根目录下的buildconfig文件夹中分别新增了publish.jswebpack.publish.conf.jspublish.env.js文件,并查阅了webpack文档,去掉了不需要的一些功能配置,设置好了对你的组件库进行打包的配置。

你期望将打包后的代码就放在你的组件库文件夹内,并命名为dist,这时你的组件库的源文件就需要移动到src目录下。

组件库源码与打包后的代码并存

webpack在对代码进行打包时需要指定入口文件,这时你发现你的组件库本身还没有出口文件。

1.6 全量加载和按需加载

你在组件库的src文件夹下新建了一个index.js文件,它引入并输出了所有的组件。

import Button from './components/button'
import Icon from './components/icon'
// ...省略的代码...
export {
  Button,
  Icon
  // ...省略的代码...
}

到这里,你或许会干脆将组件库本身的文件结构也一并规划好:

组件库本身文件结构

在这种输出格式下,你的用户可以通过

import { Button } from 'admin-ui'

来从组件库中获得Button组件。然而这仅仅是对这种格式的支持(这并不是按需加载),用户还需要能够进行全量加载,也就是一次引入所有组件并全部自动注册好。所以你在index.js中将所有的组件都挂载到adminUi对象上,然后再在该对象上挂载install()方法用于支持Vue.use(),最后直接输出这个对象。现在你的index.js看起来像这样:

import Button from './components/button'
import Icon from './components/icon'
// ...省略的代码...
export {
  Button,
  Icon
  // ...省略的代码...
}

const adminUi = {
  Button,
  Icon,
  // ...省略的代码...
}

adminUi.install = function (Vue, options = {}) {
  Vue.component('cu-button', Button)
  Vue.component('cu-icon', Icon)
  // ...省略的代码,你也可以用循环来写...
}
export default adminUi

install()方法中可以做很多事情,除了注册组件,很可能你也会在其中进行一些实例方法的挂载

这时你的用户可以通过

import adminUi from 'adminUi'
Vue.use(adminUi)

来进行全量加载。

接下来就是按需加载。你发现如果仅仅是通过你的index.js入口文件去加载某个组件,其它组件虽然没有被用户引入,但仍旧被编译到了用户的代码中去了。所以你不得不考虑新的方式。既然不能从单一入口加载,是否可以为每个组件指定一个加载点呢?你希望你的用户能够通过类似

import Button from 'admin-ui/button'

这样的方式来加载单个组件,这样就不存在多余的组件了。所以你意识到,每个组件还需要单独进行打包。以每个组件的出口文件(可能也是个index.js,这里你应该意识到每个组件的文件结构保持一致能带来好处)为打包入口,将每个组件都打包为一个单独的模块放置到dist中的lib文件夹下。这时,按需加载就被支持了。

打包后的组件库的文件结构

我并未讨论具体的webpack配置,一是因为本文主要讨论思路而不是具体实现,二是这个话题如果要深入讨论需要更多篇幅,三是webpack本身配置非常复杂而我并不算熟练。

然后你愉快地尝试了一下打包,但沮丧地发现,不管是以组件库本身的出口文件为入口,还是在对每个组件进行单独打包的时候,结果除了一个.js文件,还会有一个.css文件。你的用户不管是全量加载,还是按需加载,在引入.js文件时还要引入对应的.css文件。在全量加载时,由于只加载一次,这似乎不是什么大问题。但如果是按需加载,因为要引入多次,这就有些麻烦了。

分离的css文件是出于性能考虑,css文件可以被浏览器缓存,同时组件本身渲染时不需要再生成css了

解决方案有两种,一种是推荐用户使用babel-plugin-component,另一种是打包后的组件本身不再提供css文件,而是全局引入全量加载的那个css文件。两种做法都可以,但我使用的是后者。

有两个原因,首先组件们的样式集合起来体积并不大,压缩打包后控制在60KB以内(这其中绝大部分都是font-awesome的样式代码,组件的所有样式不超过5kb);其次由于使用了font-awesome,如果每个组件单独引入自己的样式,依赖了font-awesome的组件们就会出现重复的样式。

2 设计一个主题系统

当你的组件库被用于不同的项目中,或者某个项目需要换肤功能时,不可避免地,你需要在你的组件库中设计一个主题系统。

2.1 确定主题系统功能边界

首先需要明确,你的主题系统功能的边界。在我看来,影响一个管理类后台系统风格的因素主要有三种:

  • 颜色(这是最主要的)
  • 阴影
  • 圆角

所以不妨先将你的主题系统边界就设定为这三种因素。

2.2 选择合适的实现方案

然后你开始思考可行的主题系统实现思路:

  • 特殊格式的字符串替换
  • 主题文件预编译
  • 样式类

特殊格式的字符串替换无疑是最简便的,开发时当遇到需要被主题系统控制的样式时,在css中直接使用特使格式的字符串,在运行时进行替换即可。比如:

div {
  color: $$primary$$;
}

运行时被你的脚本替换成:

div {
  color: #00f;
}

这种方案的优点是开发时非常便捷,基本不影响开发体验,甚至还有提升。在传统的jquery时代问题不大,但就Vue项目而言,存在“替换时机”问题。你大可以在项目初始化后将页面中所有<style>标签中的特殊字符替换掉,但当页面变化,新的组件的style被插入到head中时,你还需要再次替换,很难找到合适的时机来做这件事。

主题文件预编译是目前市面上主流的主题实现方案。即UI库本身提供生成不同主题的css文件的工具,事先编译好几套不同的主题样式文件。优点是简单直接,方便好用。但缺陷也显而易见:运行时的主题替换变得非常粗暴(粗粒度)——你只能一整套一整套地替换。

样式类则是设计好样式规则,在需要的元素上应用样式类即可:

.au-theme-font-color--primary {
  color: #00f;
}
<p class="au-theme-font-color--primary">主色</p>

样式类同样有它的非常明显的缺陷:首先你需要有非常清晰的样式类规则设计,然后对开发的影响也非常重大:所有的主题系统涵盖的样式都只能用样式类来书写,不能直接写在css中。这两点给使用者带来一定认知和使用负担。但优点也同样明显:控制粒度可以非常细,不存在替换时机问题,同时,不仅仅可以控制组件库本身的主题,也可以直接用于整个项目。

带有实验性质地,我选择了样式类,所以假定你也做出了同样的选择。

2.3 使用样式类来设计和实现你的主题系统

如果你不知道从何下手,不妨试着从你的主题系统的使用者的角度入手。你期望你的使用者能够通过一个简单的函数来控制主题:

adminUi.theme(config)

那么很自然地你就会去定义好config的结构。根据前面界定好的主题系统功能,你会将其做如下定义:

const config = {
  colors: {
    'base-0': '#000',
    'base-1': '#232a29',
    'base-2': '#2f3938',
    'base-3': '#44514f ',
    'base-4': '#616e6c',
    'base-5': '#7c8886',
    'base-6': '#b1bbba',
    'base-7': '#d9dedd',
    'base-8': '#eaf0ef',
    'base-9': '#f3f7f6',
    'base-10': '#f7fcfb',
    'base-11': '#fdfdfd',
    'base-12': '#fff',

    'primary-top': '#01241d',
    'primary-up': '#3fd5b8',
    'primary': '#19bc9d',
    'primary-down': '#169f85',
    'primary-bottom': '#e7f7f4',

    'info-top': '#011725',
    'info-up': '#f0faf8',
    'info': '#3498db',
    'info-down': '#2d82ba',
    'info-bottom': '#e6f3fc',

    'warning-top': '#251800',
    'warning-up': '#fec564',
    'warning': '#ffb433',
    'warning-down': '#e99b14',
    'warning-bottom': '#fbf3e5',

    'danger-top': '#220401',
    'danger-up': '#f56354',
    'danger': '#e74c3c',
    'danger-down': '#c33a2c',
    'danger-bottom': '#fae7e5',

    'success-top': '#012401',
    'success-up': '#7fcb7f',
    'success': '#5cb95c',
    'success-down': '#3da63d',
    'success-bottom': '#e7fae7'
  },
  shadows: {
    'base': '0 1px 4px rgba(0, 0, 0, .2)',
    'primary': '0 0 4px rgba(25, 188, 157, .6)',
    'info': '0 0 4px rgba(52, 152, 219, .6)',
    'warning': '0 0 4px rgba(255, 180, 51, .6)',
    'danger': '0 0 4px rgba(231, 76, 60, .6)',
    'success': '0 0 4px rgba(92, 185, 92, .6)'
  },
  radiuses: {
    'small': '2px',
    'large': '5px'
  }
}
  • primarywarningdangerinfosuccess为主要颜色
  • [COLOR]-up[COLOR]-down 为明度较接近主要颜色的次要颜色
  • [COLOR]-top[COLOR]-bottom 为明度与主要颜色相差较大的辅助颜色
  • base-0base-12 为最暗无彩色和最亮无彩色
  • base-[1~11]为按明度排列的无彩色(灰色)

不使用带有颜色信息的词(比如light、dark-primary等)而是使用数字和方向来作为颜色名称的原因是为了方便用户在某个名称上定义任意的颜色,假如你将纯黑色的名称定义为了dark,但用户配置时却使用的是#fff纯白色,这个名称就会带来误解。在非彩色上,我们使用数字来作为名称,而在彩色上,使用方向来作为名称,既能契合彩色的层次设计,又能规避歧义。

你的这套配置规则期望用户能够按照明度配置颜色,每个种类颜色明度排列都是一致的。这是为了方便色彩之间的明暗搭配,比如应该在深色背景上使用浅色文字。但有这个限制的同时,带来的好处便是用户能够配置一些自定义的颜色。

同时,为了进一步精简颜色配置,你决定在阴影、非主要颜色和无彩色缺省的情况下,基于primary颜色和一些辅助配置来自动计算它们。于是用户的实际配置可以进一步简化:

export default {
  theme: {
    colors: { // 彩色配置
      primary: '#1c86e2',
      info: '#68217a',
      warning: '#f5ae08',
      danger: '#ea3a46',
      success: '#0cb470'
    },
    shadows: { // 阴影配置
      // primary: '0 0 4px #1c86e2',
      // info: '0 0 4px #68217a',
      // warning: '0 0 4px #f5ae08',
      // danger: '0 0 4px #ea3a46',
      // success: '0 0 4px #0cb470'
    },
    radiuses: {
      small: '3px',
      large: '5px'
    }
  },
  lightnessReverse: false, // 反转lightness排序(黑白主题)
  colorTopBottom: 5, // top和bottom颜色距离纯黑和纯白的lightness的距离,越小越接近纯黑纯白
  colorUpDown: 10, // 彩色上下接近色与正色的lightness距离
  baseColorLeve: 12, // 无彩色分级数量
  baseColorHue: '20%', // 无彩色饱和度
  baseShadowOpacity: 0.2, // 无彩色阴影不透明度
  colorShadowOpacity: 0.6 // 彩色阴影不透明度
}

主题系统的文件结构如下:


主题系统文件结构

接下来,思考用户配置完了主题系统后,他们如何将其应用到元素上。你的主题系统提供的样式类,需要一个便于记忆的语法,来方便用户使用,这时你可能会设计出类似下面这样的语法规则:

前缀 [-伪类名] -属性名 --属性值 [-权重]
  • 前缀:主题样式类前缀
  • 伪类名:可选的,如果主题是应用在当前元素的伪类上的,则可以在类名中连接伪类名
  • 属性名:样式的属性名
  • 属性值:样式的属性值,即在config中配置好的名称
  • 权重:可选的,可使用其为该主题样式添加!important后缀

在这套语法规则下,用户用起来就像下面这样:

<div class="
  au-theme-background-color--base-12
  au-theme-border-color--primary
  au-theme-font-color--base-3
  au-theme-box-shadow--base
  au-theme-radius--small"></div>

最后,你的主题系统将用户传入的配置根据你的语法规则转换为具体的样式类代码,并利用<style>标签将其插入了页面。

3 提供一套表单组件

任何一个UI组件库,尤其是管理系统的UI组件库,都不可避免地需要提供一套表单组件。原因很简单,首先各家浏览器提供的默认表单控件不仅风格不一还丑到天际;其次表单的排版、验证等等功能都是刚需,没理由不抽象出来。

所以首先你会列举出常用的表单组件:文本输入框、多选、单选、开关、下拉、级联、日期、时间、文件上传,这些组件都被你放到TODO LIST中了。

3.1 统一表单接口

你会发现很多表单组件的行为方式是一致的,比如都需要value接口,都得支持v-model,都有input或者change事件等等。这些统一的行为最好放置到一起去,所以你使用Vue的mixin功能来提取这些统一的行为到一起,一方面便于管理,另一方面能够使表单组件在功能与行为上尽可能保持一致,以此来降低用户的认知成本。

3.2 统一验证方式

其实验证这部分功能严格来讲,也算是统一的表单接口,所以也可以一并放在上面的文件中,但验证部分的逻辑其实相对独立一些,所以你很可能会将其独立出来另做一个minxin来管理。

如果你经常编写表单,不难发现实际上关于验证,只有两种情况:

  • 交互验证:用户在填写某个表单元素时触发了该元素的验证
  • 提交验证:用户在提交时触发了整个表单所有元素的验证

要支持交互验证其实很简单,简单利用事件即可实现。而要支持提交验证,则需要每个表单组件提供具体的验证方法供外部调用,比如:this.$refs.$userNameInput.validate(),外部调用该函数即可获得验证结果;在提交表单时将所有表单组件的验证方法调用一遍即可。

而你的程序在运行验证代码时,也有两种情况:

  • 同步验证
  • 异步验证

支持同步验证非常简单,正常调用外部给定的验证器函数,然后返回其结果即可。但如果是异步验证就会比较麻烦。我们稍微深入一点,假设目前用户像下面这样指定了<au-input/>的验证器:

<au-input
  :validatiors="[
    {
      validator (v) { return v > 0 },
      warning: '必须大于0'
    }
  ]"/>

当你获取到这个验证器后,你并不能知道其是同步还是异步验证,所以你可能会要求用户指明是同步还是异步:

<au-input
  :validatiors="validators"/>
export default {
  data () {
    return {
      validators: [
        {
          validator (v) { return v && v.length && !/^\s*$/g.test(v) },
          warning: '不能为空'
        },
        {
          validator () {
            return new Promise(resolve => {
              axios.get('is-duplicated-name')
                .then(data => resolve(data.result))
            })
          },
          warning: '已经有重复的名字了',
          async: true
        }
      ]
    }
  }
}

当用户像上面一样指明了同步还是异步验证,并且其验证函数返回的是一个promise后,你就可以事先将所有的验证器分成两类:同步验证和异步验证,并且首先验证同步函数,如果有任意未通过的验证,则可以不验证异步函数来减小开支。下面是我在Admin UI中的验证逻辑,放出来供大家参考:

// the common validation logic of enhanced form components
export default {
  // ... 省略的代码 ...
  methods: {
    validate () {
      let vm = this
      if (vm.warnings && vm.warnings.length) return false
      if (!vm.validators) return false
      // separate async and sync
      let syncStack = []
      let asyncStack = []
      vm.validators.forEach((v) => {
        if (v.async) {
          asyncStack.push(v)
        } else {
          syncStack.push(v)
        }
      })

      // handler warnings
      let handleWarnings = (res, i, warning) => {
        if (!res) {
          vm.$set(vm.localWarnings, i, warning)
        } else {
          vm.$delete(vm.localWarnings, i)
        }
      }

      return new Promise((resolve) => {
        let asyncCount = asyncStack.length
        // execute sync validation first
        syncStack.forEach((v, i) => {
          handleWarnings(v.validator(vm.value), i, v.warning)
        })
        // if sync validation passed, execute async validationg
        if (!vm.hasLocalWarnings) {
          if (asyncCount <= 0) { // no async
            resolve(!vm.hasLocalWarnings)
          } else {
            Promise.all(asyncStack.map((av, i) => {
              return av.validator(vm.value).then(res => {
                handleWarnings(res, i, av.warning)
              })
            })).then(results => {
              if (results.includes(false)) resolve(!vm.hasLocalWarnings)
              else resolve(!vm.hasLocalWarnings)
            })
          }
        } else { // if sync validation failed
          resolve(!vm.hasLocalWarnings)
        }
      })
    }
  }
}

表单组件的验证方法返回的是一个promise,在其resolve方法中返回了具体的验证结果,好处是在提交验证时,用户不需要区分同步还是异步,全部统一对待,简单方便:

export default {
  validateAllFormitems () {
    Promise.all([
      this.$refs.name.validate(),
      this.$refs.age.validate(),
      this.$refs.gender.validate()
    ]).then(results => {
      if (!results.includes(false)) this.submit()
    })
  }
}

3.3 封装排版

常见的表单排班有两种,一种是label与表单元素上下排列,另一种是左右排列。你的表单组件们的排版方式应当保持统一,所以你可能会创建一个表单组件的容器组件来做这件事。当然,你的表单组件接口中,也应当有对应的控制排版的接口。

上下排列时,除了常规宽度,像文本输入框、下拉选择框一类的组件还要考虑100%宽度的情形。这时你可能需要另一个full-width接口来让用户选择是否全宽度占满。

左右排列时,则要考虑label的对齐。一般的做法是,规定所有表单元素的label到一个合适的宽度,之后label中的文字向右对齐。由于各个组件之间本身是相互独立的,你应该会期望使用者来告诉你label的合适宽度,所以你每个表单组件都提供了label-width接口。

你将这些特性都封装在一个叫form-item的容器组件中,并在每个表单组件中使用它。

3.4 日期、时间和日期时间范围

关于日期、时间及日期时间范围这三个功能对应的组件,我希望你能够思考的问题是:如何划分组件的功能。

常见的划分是

  • 日期选择器:可以选择单点日期,也可以选择日期范围
  • 时间选择器:可以选择单点时间,也可以选择时间范围
  • 日期时间选择器:可以选择单点日期和时间,也可以选择日期+时间的范围

然而我更推荐的划分是

  • 日期选择器:仅能选择单点日期
  • 时间选择器:仅能选择单点时间
  • 日期时间范围选择器:仅能选择范围,但可以只是日期范围,也可以只是时间范围,也可以是日期+时间的范围

这么划分的好处是,你的日期选择器和时间选择器能够最大程度被复用,并且三个组件在实现上相对前一种划分中的三个组件要简单很多。这并不好理解,需要你仔细体会。

4 提供一套图标库

绝大部分UI库都提供了图标组件。原因很简单:没有人喜欢那烦人的字体文件路径问题。将字体文件通过一个固定的组件进行引入能够避免你的使用者为其所困。

Admin UI早期版本使用的是一个更漂亮的Ionicons图标库,但其图标种类略少,后续的版本更换成了Font Awesome图标库。

选择何种图标影响并不是很大,你甚至可以不使用地第三方的图标库而是仅提供你的组件库所需要的最小图标集,转而将使用何种图标库的选择权交给你的使用者——图标组件应当支持第三方图标的使用。

5 必要的网格系统

市面上几乎绝大部分UI库都带着网格系统,来方便开发者快速自适应布局页面。早期的技术,例如Bootstrap等UI库,使用floatwidth等CSS属性及CSS媒体查询来实现网格系统。而现代的UI库则大部分使用flex来实现网格。

你可能会想要使用现代的技术来实现一个类似Bootstrap中的网格系统。然而在Bootstrap中,网格的使用是依靠样式类来进行的,同时它要求一个父元素及若干子元素来形成要求的结构。你可能对此并不满意。样式类的应用可以使用props来替代,而固定的父级元素,你却可能并不希望用户关心。为此,你考虑只用一个grid组件来实现整个网格系统,所以在初始化的时候,你需要处理好父元素,尤其是需要处理父元素涉及使用display属性的情况,因为你需要总是在父元素上使用flex属性。

在不考虑格子之间间距的情况下,完全可以使用媒体查询和flex属性来完成网格系统。但如果涉及到间距,要使用CSS来实现就会比较麻烦。flex属性能够实现网格横向排列的特性,但网格本身宽度,因为涉及到间距的计算,你可能会使用JavaScript来做。

使用者通过props传入一个类似widthLg的属性,告知组件在大屏下所占的网格数量,通过space属性告知组件与下一组件的间距,这时候需要引入行的概念,你需要计算哪些组件处在一行中,因为行末尾的最后一个grid右侧不能有间距,否则会因为超出一行总宽度而被挤到下一行。

网格的实现并不困难,主要面对的需求点就是网格自己的特性:不同屏宽下所占网格数量、偏移距离、间距。你可能看明白了,我并不打算展开讲具体实现,但是你可以去看看源码一探究竟。说实话,这部分的实现并不算有优雅,主要是一些实验性质的做法。欢迎你来重构!

6 单元测试和国际化

说实话,这一部分我并不打算讲。但你得知道,你的组件库未来如果要开源,这两部分是必不可少的。

7 结语

如果以具体实现细节作为考量,这篇文章基本上没有什么干货。我仅仅是把编写一个组件库需要面对的问题进行了罗列,并泛泛地谈了谈解决方案而已。不管怎样,希望能够对你有所启发。

最后,即使是造轮子,也有它其中的乐趣所在。这里先挖一个坑,未来我可能会从组件中挑选一些比较有意思的,另写文章来分享具体的组件实现细节,敬请期待~

同时,也欢迎你去fork这个充满了私货的UI库,提Issues或者Pull Request都是非常欢迎的~

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,117评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,963评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,897评论 0 240
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,805评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,208评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,535评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,797评论 2 311
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,493评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,215评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,477评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,988评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,325评论 2 252
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,971评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,055评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,807评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,544评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,455评论 2 266

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 170,563评论 25 707
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,099评论 18 139
  • 3月4日日精进:敬畏—进入—体验—交给—持续 1,缺啥补啥,怕啥练啥; 2,一切为我所用,所用为团队家; 3,我...
    54f0d725963c阅读 143评论 0 0
  • 做一只优雅的狐狸精 说到狐狸精,似乎是个老话题了,在《封神榜》里,对九尾狐化身的苏妲己并没有太多的好感。这个祸国殃...
    梦影的花园阅读 1,048评论 3 5
  • 现在怎么还有这么不懂事的人呢?是不知道警察叔叔有多辛苦么? 好吧,有这么多热心市民贡献素材,警察叔叔们的日常似乎也...
    青神科技客户部阅读 181评论 0 0