当css flex 遇到 Vue指令

avatar
@https://www.tuya.com/

作者: 涂鸦-Cheetah

来涂鸦工作: job.tuya.com/


css flex 与 Vue 指令的完美结合

flex弹性盒子是最流行的布局属性,可以帮助我们快速完成响应式页面。 在开发vue时,我们会应用大量flex样式,这就需要我们去维护好我们的css代码。

痛点

你有遇到这样的问题吗?刚开始不需要flex布局,当新元素加入时,又突然需要使用flex进行对齐或者排版, 所以就不得不折返到css代码区添加flex样式。
这样会比较麻烦,尤其是代码很多时,需要在海量代码里全局搜索,而且会出现大量冗余代码。

解决方法

其实我们可以将flex有关的样式更加细分,并提取成通用css代码(不要在通用代码名称里出现与业务有关的东西),比如.flex, .flex-wrap, .flex-center等等,在需要的时候选择并组合自己所需的样式即可,这样可以避免复制粘贴冗余代码。

更好的解决方法

不过有更好,更vue的方法吗?可以让我们随时随地,想用就用而不必考虑css代码呢?

Vue指令

Vue为我们提供了自定义指令功能,我们已经用过一些内置指令比如v-model, v-show等,那么今天我们将使用指令去封装所有与flex有关的代码。

指令可以用来操作dom,一个指令其实就是在不同时机(钩子函数)去操作dom的方法

// 注册一个全局自定义指令 `v-autofocus`
Vue.directive('autofocus', {
  // 当被绑定的元素插入到 DOM 中时执行
  inserted: function (el, binding) {
    // el为当前dom
    // binding是一个对象里面包含指令名,modifier, srg以及表达式等等
  }

  //当然还有其他钩子函数
})

若你从未接触过指令,不要担心,它很简单,你可以请先阅读 官方教程,因为此文章是技巧经验分享,而不是Vue教程!

CSS flex回顾

那么在开始之前我们快速回顾下flex的一些属性以帮助我们更好的设计指令。

flex盒子由flex容器(以下简称容器)和内部的子flex item,(以下简称item)两部分组成。 flex容器用于指定其子item在容器内的排列方向,如何排列,对齐,是否换行等等; item用于指定自身在容器内如何对齐,当然更重的就是自身如何伸展和收缩,排列顺序,这几个才是真正能体现弹性的属性;

我们总结一下上述的属性,因为在写指令时要用到,如果你已经会了,下面的内容对你来说有点啰嗦,请直接移步至指令设计部分.

首先我们先要区分出哪些属性用于容器,哪些用于item.
用于容器的属性:

display:

声明为flex容器

  • flex: 块级flex容器
  • inline-flex: 行内flex布局

flex-direction

指定item的排列方向:

  • row: 横向排列item
  • column: 竖直排列item

flex-wrap:

当在排列方向上没有足够空间时,换行排列item(想想下做广播体操时,一行人占不满就多排几行的场景)

justify-content:

控制item在容器中如何排列:

  • flex-start: item统一靠着容器头部排列,横排时为左侧,竖排时为顶部
  • flex-end: item统一靠着容器尾部排列,横排时为右侧,竖排时为底部
  • center:所有item都集中在容器中心
  • space-between: item向着容器两边均匀排列分布,每个item之间相隔有均等的空隙(如果item没有占满容器,容器有剩余空间),容器两侧的item与容器无空隙
  • space-around: 与space-between类似,只不过容器两边的item与容器有均等的空隙

align-item:

控制如何对齐元素,对齐方向与排列方向相反:

  • strech: item完全填满父容器
  • flex-start: item靠着容器头部排列,横排时为顶部,竖排时为左侧
  • flex-end: item靠着容器尾部排列,横排时为底部,竖排时为右侧
  • center: 所有item都处在容器中心线对齐
  • space-between: item向两边均匀分布,每个item之间相隔有均等的空隙(如果item没有占满容器,容器有剩余空间未占用),容器两侧的item与容器无空隙
  • space-around: 与space-between类似,只不过容器两边的item与容器有空隙

其他关于item专用的flex属性,出于时间问题,这里不再赘述了,可参考下面的官方文档 developer.mozilla.org/zh-CN/docs/…

设计指令

指令其实是一个方法,vue合适的时机调用我们的方法并传入指令参数和节点,我们拿到参数后去处理节点等. 现在我们开始设计我们的指令

指令名与执行时机

  1. 选取执行指令的时机 根据你的项目需要来指定,在本章教程我们仅在bind, update时执行,因为我们目的用于修改样式且不涉及添加listener等操作,且无需在销毁时做一些清理工作.

    • bind:只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。
    • update:所在组件的 VNode 更新时调用,但是可能发生在其子 VNode 更新之前。指令的值可能发生了改变,也可能没有。但是你可以通过比较更新前后的值来忽略不必要的模板更新 (详细的钩子函数参数见下)。
  2. 我们根据使用dom对象来将指令分成两个:

    • v-flex:用于容器
    • v-flex-item: 用于item 让然如果你想简写,也可以写成v-flx, v-itm,但v-前缀是Vue要求的不可省略
  3. 传参并从指令取值
    请先学习指令参数教程
    从官方文档中得知我们可以接收多个参数,那么本文我们仅关心如下参数:

    • arg:传给指令的参数,可选。例如 v-my-directive:foo 中,参数为 "foo"。
    • modifiers:一个包含修饰符的对象。例如:v-my-directive.foo.bar 中,修饰符对象为
      { foo: true, bar: true }
      

设计modifier

从上面我们得知modifier类型为boolean.

  • 有一种使用场景就是:属性值是互斥的,只可选一个,这刚好对应了truefalse;
  • 另外一种场景就是用于指定某个属性的值,很可能这个属性有多个值可选,且该属性频繁使用
  • 当然modifier可以代表一种预设名,通过预设名来添加一套css代码,
    因此我们可以这样设计:
  1. 取值互斥的flex属性有:

    • display: inline-flex | flex
      inline(行内)和flex(块级)从来都是互斥,默认flex,所以默认代表false, inline代表true,我们可以将inline作为modifier,像下面这样: v-flex.inline 等价于

      {
        diaplay: inline-flex;
      }
      

      那么js代码也比较简单,直接添加style即可

      Vue.directive('flex', {
        bind: function (el, binding) {
          let { inline } = binding.modifiers //判断是否有inline的modifier
          el.style.display = inline ? 'inline-flex', 'flex'
        }
      })
      
    • flex-direction,两者取值也是互斥,要么横排(row)要么竖排(column),横排是默认,因此设置column作为modifier,如下: v-flex.column 等价于

      {
        flex-direction: column;
      }
      

      注意: direction还可以反向排列,这边我们将reverse作为modifier来标记反向:
      v-flex.reverse 等价于 { flex-direction: row-reverse; }
      v-flex.column.reverse 等价于 { flex-direction: column-reverse }

    • flex-wrap的取值也互斥,类似地:
      v-flex.wrap 等价于:

      {
        flex-wrap: wrap
      }
      
  2. 使用某个属性值作为modifier
    我们注意到justify-contentalign-items是我们使用比较频繁的属性,我们可以将其设置modifier以便快速添加样式,就像下面这样: v-flex-justifyFlexStart 等价于

    {
      justify-content: flex-start;
    }
    

    注意到了吗?这样的写法首先字数太长,不便于记忆,更不方便后续开发人员接手与维护。其次,从指令里难以预测到最终的UI效果。因为justify和align并不能直观告诉我们排列方向

    我们发现justify-contentalign-items虽然取值一样,但是他们刚好代表相反的方向: 水平或者竖直
    因此我们可以用**语义化的modifier**代替原始css属性:

    • 我们将水平的排列(或者对齐)用toLeft, toRight, toCenter标记
    • 将竖直的排列(或者对齐)用toTop, toBottom, toModdile标记
    • 另外还有个特殊的stretch为什么我们没有处理?原因是他是align-items的默认值,无需特别处理
    • 同样的还有space-betweenspace-around,不要把这两个漏了,他们仅适用于align-items

    通过上述语义化modifier,这样我们就可以直观的从指令中预测出UI的布局

    使用者不需要知道modifier究竟对应justify-contentalign-items哪一个,他们只需要关心朝哪个方向排列即可。因为我们已经在代码里handle下:

    • 当你的directionrow时,toLeft...一组对应justify-content; toTop...一组对应align-items
    • directioncolumn时,相反

    哦,对了,我们还需要一个额外的modifier,那就是center
    在很多场景中,我们都会使用直接用flex布局,让一个子元素在容器里始终水平竖直都居中,比如对话框等等,这可比因此我们这样设计: v-flex.center等价于

      {
        justify-content: center;
        align-items: center;
      }
    
  3. 预设
    你可以根据你的需要指定预设,比如我有时候时候希望flex容器的宽度为包裹内容的宽度,比如一盒横向滑动的swiper,因此我增加fitContent作为一个modifier,以添加如下预设css代码: v-flex-fitContent 等价于:

    {
      width: fic-content;
      flex-basis: auto;
    }
    

    或者添加一个预设noScroll以禁止超出容器:
    v-flex-noScroll 等价于:

      {
        overflow: hidden;
      }
    

当然你还可以根据项目需要来添加更多预设。
我们还剩余argexpression没有使用,由于modifier已经涵盖了大部分属性,在本文里看似已经不需要了,当然你也可以充分利用它,根据你们项目来使用。

  • 比如你希望由flag控制flex指令的启用与禁用,那么你可以这样写 v-flex="isSupportFlex"并在代码里判断flag
  • 特别地,expression还支持字面量对象,通过传入对象即可更大限度的拓展我们的指令功能,比如媒体查询等等

代码实现

实现方式为直接操作css,比较简单,具体代码可参考这个repo: github.com/BingLee1994… 另外还有item指令,思想都一样,本文那就不再赘述了。 截屏2020-11-12 下午10.14.41.png 更多例子请参考: github.com/BingLee1994…

总结

本文使用了指令来封装css flex

  • 设计指令要选取好合适的时机(钩子方法)
  • 本文大部分属性均通过modifier来实现,因为modifier可用于那些互斥的,二选一的css属性,还可用于启用/禁用某些预设,亦或者直接是某个属性的值.
  • expression用于向指令传递更丰富的数据,甚至可以支持传递字面量对象,方便我们拓展指令功能.
  • arg可指定参数名,用来说明的expression的用途等等.

那么还有其他场景也可以使用指令,最常见的比如加loading动画,loadingSpinner等等
或者使用IntersectionObserver来创建懒加载的lazy-image等等,不过特别注意的是,对于需要添加event listener的指令,不要忘了随着组件销毁,也要及时清理listener.


来涂鸦工作: job.tuya.com/