专业编程基础技术教程

网站首页 > 基础教程 正文

基于Vue3的前端网格布局探索:使用JS动态生成响应式网格布局

ccvgpt 2024-12-22 14:46:08 基础教程 1 ℃

提示:文章底部有完整的源代码,使用Rollup编译后总体积只有2kb左右,童鞋们有需要可以直接CTRL + C拿走。

我想,前端的童鞋们应该都了解或使用过网格布局吧?著名的UI库:bootstrap,element-ui,iview等都提供了网格布局。我们所熟知的这些库都是采用的CSS预处理语言生成的网格布局。这是一种主流的实现方案,也很符合前端开发的一个格言:能用CSS实现的,尽量不要用JS实现。

基于Vue3的前端网格布局探索:使用JS动态生成响应式网格布局

动机:熟悉网格布局实现原理的童鞋们应该都比较清楚,网格布局的CSS代码量是很大的,轻而易举就能达到30kb以上;虽然我们是用less这样的预处理语言写的,拥有编程语言的一些能力,可以利用循环和变量来大幅减小我们所编写的代码量;但是,最终浏览器加载的依旧是生成的CSS。这个体积嘛,像我这种对体积非常敏感的人,感觉有一点点儿大。那么,我们有没有一种方式,可以大幅减小网格布局的体积呢?这正是本篇文章的主题:使用JS动态生成响应式网格布局。

我们先看两张效果图,这是element-ui官方文档Row和Col组件的例子没有做任何修改,直接在本篇文章实现的组件下的效果。



要使用JS生成网格布局,我们需要动态创建样式表;使用JS创建样式表是很简单的事情,只需要创建一个style元素,然后将样式表字符串添加到style元素,最后将style元素添加到head即可。如下是createStylesheet函数的定义:

export function createStylesheet (id, styleSheetStr) {
  let el = document.getElementById(id)
  // 避免重复创建相同的样式表,只有不存在的时候才创建
  if (!el) {
    el = document.createElement('style')
    el.id = id
    el.innerHTML = styleSheetStr
    document.head.appendChild(el)
  }
}

我们采用的网格布局是24分栏,和element-ui保持一致,这也是目前最主流的网格布局分栏数量。我们需要生成0-24共25列,当列占用的空间为0的时候处于隐藏状态。现在,我们先创建一个包含25个元素的数组,我们不需要关注数组中元素的值,我们只会用到元素的索引。之所以使用数组,是因为我不想使用for循环,而更偏向于数组的遍历方法。

const nulls = new Array(25).fill(null)

现在,我们定义一个获取列宽度的函数getSpan,当列数为0的时候,将元素设置为不可见。

const getSpan = (i, val) => i ? `width:${val}` : 'display:none'

然后,我们创建用于生成列的函数genCol,该函数将列数转化为百分比,以实现弹性的宽度。不知道童鞋们有没有被left和right搞懵呢?[呆无辜]

export const cls = 'x-col' // class前缀

const genCol = () => nulls.map((_, i) => {
  const val = `${i / 24 * 100}%`
  return [
    `.${cls}_span-${i}{${getSpan(i, val)};}`, // 列宽
    `.${cls}_pull-${i}{right:${val};}`, // 向左移动的宽度
    `.${cls}_push-${i}{left:${val};}`, // 向右移动的宽度
    `.${cls}_offset-${i}{margin-left:${val};}` // 向右的偏移宽度
  ].join('')
}).join('')

目前,我们生成的布局不是响应式的,不管屏幕有多宽,都会占用固定的百分比宽度。那么,我们如何使布局变成响应式的呢?媒体查询,该你出场了。

现在,我们先定义一个根据窗口宽度生成布局的函数genColBySize。这个函数和上面的genCol函数长的很像,只是class名称中添加了一个size,童鞋们应该都能理解吧?

const genColBySize = size => nulls.map((_, i) => {
  const val = `${i / 24 * 100}%`
  return [
    `.${cls}_${size}-span-${i}{${getSpan(i, val)};}`,
    `.${cls}_${size}-pull-${i}{right:${val};}`,
    `.${cls}_${size}-push-${i}{left:${val};}`,
    `.${cls}_${size}-offset-${i}{margin-left:${val};}`
  ].join('')
}).join('')

我们与element-ui保持一致,将响应式断点设置为5个,分别是: xs,sm,md,lg,xl;现在,我们生成响应式布局,并导出一个添加样式表函数addStylesheet。

const genResponsiveCol = () => [
  ['xs'],
  ['sm', 768],
  ['md', 992],
  ['lg', 1200],
  ['xl', 1920]
].map(_ => _[1]
      ? `@media (min-width:${_[1]}px){${genColBySize(_[0])}}`
      : genColBySize(_[0])
 ).join('')

// 为什么没有写在addStylesheet里面?
// 是为了减少2个生成函数的调用次数,避免不必要的调用,现在只会被调用一次
const ruleStr = genCol() + genResponsiveCol()

export const addStylesheet = () => {
  createStylesheet('XGridLayout', ruleStr)
}

以上就是使用JS生成响应式网格布局的全部核心代码,是不是很简单?现在,我把Row和Col组件的剩余代码提供给童鞋们,为了节省篇幅,把空行去掉了,但可读性还是很高的。希望阅读过本篇文章的童鞋们都能够自己动手实现。

Col.vue组件源码:

<template>
  <div :class="classes" :style="styles">
    <slot />
  </div>
</template>
<script setup>
import { computed, inject, onMounted } from 'vue'
// N: Number, N0: { type: Number, default: 0 }
import { N, N0 } from '../../types'
import { addStylesheet, cls } from './utils'
const props = defineProps({
  span: { type: N,  default: 24 },
  offset: N0,
  push: N0,
  pull: N0,
  xs: {},
  sm: {},
  md: {},
  lg: {},
  xl: {}
})
const classes = computed(() => {
  const clsList = [cls]
  ;['span', 'offset', 'push', 'pull'].forEach(k => {
    const v = +props[k]
    v && clsList.push(`${cls}_${k}-${v}`)
  })
  ;['xs', 'sm', 'md', 'lg', 'xl'].forEach(k => {
    const v = props[k]
    if (v) {
      const opts = +v ? { span: +v } : v
      Object.keys(opts).forEach(k2 => {
        clsList.push(`${cls}_${k}-${k2}-${opts[k2]}`)
      })
    }
  })
  return clsList
})
const gutter = inject('gutter') // 响应式的数值,由Row组件提供,注入到Col组件
const styles = computed(() => {
  const padding = `${gutter.value / 2}px`
  return gutter.value && { paddingLeft: padding, paddingRight: padding }
})
onMounted(() => {
  addStylesheet()
})
</script>

Row.vue组件源码:

<template>
  <div :class="classes" :style="styles">
    <slot />
  </div>
</template>
<script setup>
import { computed, provide, toRefs } from 'vue'
// N0: { type: Number, default: 0 }, oneOf: (arr, v) => arr.includes(v)
import { N0, oneOf } from '../../types'
const props = defineProps({
  gutter: N0,
  justify: {
    default: 'start',
    validator: v => oneOf(['start', 'end', 'center', 'space-around', 'space-between'], v)
  },
  align: {
    validator: v => oneOf(['top', 'middle', 'bottom'], v)
  }
})
const { gutter } = toRefs(props) // gutter是响应式的
provide('gutter', gutter) // 提供给子组件使用
const classes = computed(() => {
  const cls = 'x-row'
  return [
    cls,
    props.align && `${cls}_${props.align}`,
    props.justify && `${cls}_${props.justify}`,
    { gutter: props.gutter }
  ]
})
const styles = computed(() => props.gutter && { margin: `0 -${props.gutter / 2}px` })
</script>

Col组件样式是通过JS生成的,我们只需要row.scss样式文件就够了,这里是源码:

.x-row {
  display: flex;
  &_top {
    align-items: flex-start;
  }
  &_middle {
    align-items: center;
  }
  &_bottom {
    align-items: flex-end;
  }
  &_start {
    justify-content: flex-start;
  }
  &_end {
    justify-content: flex-end;
  }
  &_center {
    justify-content: center;
  }
  &_space-around {
    justify-content: space-around;
  }
  &_space-between {
    justify-content: space-between;
  }
}
.x-col {
  word-wrap: break-word;
}

现在我们可以实现体积只有2kb的响应式网格布局了,童鞋们理解了吗?感谢阅读!

Tags:

最近发表
标签列表