3.1 简单轮子 VueGrid:网格系统 ⇧
大纲链接 §
[toc]
大纲
- 什么是
Grid System
网格系统 - 组件UI
- 组件代码
- 单元测试
什么是 网格系统/栅格系统 Grid System
⇧
- 知乎问答:什么是栅格化设计
- 就是把一个 div 分成 N 个部分(N = 12, 16, 24…),每个部分无空隙或者有空隙
一些名词概念 ⇧
栅格
:grid
布局
:layout
空隙
:gutter
偏移
:offset
跨度
:span
分别有12 x 2
8 x 3
6 x 4
4 x 6
3 x 8
2 x 12
组件UI结构 ⇧
|
|
flex
布局- 横向布局
- 纵向布局
- 有无空隙
gutter
- 自适应 or 响应式
- 只要是
flex
布局都可以是自适应的 - 响应式专指利用媒体查询
@media
做各种客户端的适配
- 只要是
API设计 ⇧
父组件
<vue-row></vue-row>
- 参数:
align
gutter
colData
子组件
<vue-col></vue-col>
- 参数:
span
offset
mobile
pad
laptop
pc
pcw
pcx
组件UI设计 ⇧
设计图
语雀设计稿
- 链接
- 采用 24 栅格系统
参考
gulu UI
组件代码 ⇧
实现基本样式 ⇧
列组件
row.vue
|
|
- 独占一整行
- 设置插槽
<slot></slot>
,存放 一个或多个 行组件
行组件
col.vue
|
|
- 设置插槽
<slot></slot>
,存放 其他元素 - 行内弹性盒
展示组件
GridSystems.vue
|
|
实现等分栅栏 ⇧
GridSystems.vue
|
|
实现不等分栅栏 ⇧
GridSystems.vue
- 一种做法是尝试使用自定义属性
data-span
定义跨度值,通过属性选择器定义样式
|
|
- 需要定义
.col.col-1 ~ .col.col-24
每一个样式- 太麻烦
- 重复
推荐使用 外部数据 + scss
循环语法 代替自定义属性 实现不等分栅栏
./src/components/GridSystems.vue
|
|
- 设置外部属性
span
./src/components/grid-system/VueCol.vue
|
|
- 外部数据的类型可以给多个类型
@Prop({type: [String, Number],}) span!: string
- 绑定样式
:class="[`col-${ span }`]"
scss
- 循环语法
@for
- 插值语法
#{xxx}
- 定义变量
$xxx: yyy
- 循环语法
为什么定义了
width: 50%;
,在父容器display: flex;
中的三个元素仍可以平均分配整个宽度
- 定义了
display: flex;
的元素 默认属性flex-wrap: nowrap;
,会使子元素 默认不换行 - 而且不换行的子元素的
flex-grow
默认为1
,自动平均撑满父元素 - 设置
flex-wrap: wrap;
,或者设置flex-shrink: 0;
不收缩,超出宽度的子元素会被挤到下一行
实现栅栏偏移 ⇧
精确地定义偏移量
offset
GridSystems.vue
传值offset
VueCol.vue
设置外部数据offset
GridSystems.vue
|
|
VueCol.vue
|
|
实现固定空隙 gutter
⇧
- 设定空隙为
10px
- 所有元素
box-sizing: border-box;
- 子元素使用
margin: 0 10px;
- 父元素使用 负
margin: 0 -20px;
消除左右内部两边多余的margin
- 左右和整个父元素宽度(页面宽度)对齐
GridSystems.vue
|
|
VueRow.vue
|
|
VueCol.vue
|
|
实现 可设置 的固定空隙 gutter
⇧
margin
值改为可设置的属性,VueRow.vue
传外部数据gutter
- 绑定
style
属性::style="{marginLeft: -gutter + 'px', marginRight: -gutter + 'px'}"
|
|
VueCol.vue
|
|
GridSystems.vue
|
|
用Vue钩子实现 添加空隙 ⇧
重复写属性
gutter
,能否只写一次,父组件拿到数据传给子组件
created
时拿到子组件``- 将
gutter
属性传入
在
VueRow.vue
中控制台打印出子组件
created() {console.log(this.$children);}
- 控制台得到空
- 点开数组,有属性
- chrome bug
const a = []; console.log(a); a.push(1);
点开数组,有属性 created
时,还没有子组件
created
和mounted
的区别
mounted() {console.log(this.$children);}
控制台得到包含子组件的数组- 类比
created
好比const div = document.createElement('div')
在内存中创建对象mounted
好比document.body.appendChild(div)
将对象挂载到页面中去
Vue.js
处理 父子组件挂载顺序,可在控制台打印:- 在
VueRow.vue
中写:created() {console.log(row created);}
mounted() {console.log(row mounted);}
- 在
VueCol.vue
中写:created() {console.log(col created);}
mounted() {console.log(col mounted);}
- 在
- 先创建父组件,再创建子组件,将子组件挂载到父组件上,将父组件挂载到根组件上
- 当父组件已经挂载到页面上,即
mounted()
时,说明父组件可取到所有子组件 - 为每个获取到的子组件添加属性
- 必须判断子组件类型是否为
VueRow
VueRow.vue
|
|
- 在
VueRow
上接受一个外部数据gutter
VueRow
将gutter
传入每一个子组件VueCol
- 注意类型声明
(vm as any).gutter = this.gutter;
- 代替写法
const source = {'gutter': this.gutter};
Object.assign(vm, source);
- 在子组件里需要声明
data
数据gutter
- 或者使用
this.$set(vm, 'gutter', gutter);
/Vue.set(vm,'gutter', gutter);
- 或者使用
Provide Inject
的装饰器写法@Provide('gutterToSon') gutterToSon = this.gutter;
来传递gutter
属性
|
|
- 在组件挂载时,调用
this.gutterToCol()
向子组件传递gutter
属性
重构VueRow
和VueCol
组件 ⇧
- 将组件标签内的js代码
:class="{...}"
提取到计算属性中computed: {getClass() {...}}
- 不提取到
data
中时因为data
只会在一开始读取引用的数据 - 当引用的数据变了,不会相应改变
- 当一个属性时依赖(引用)另一个属性时,必须使用
computed
VueCol.vue
|
|
const {span, offset} = this;
解构赋值变量style
内联样式优先级高于class
span
默认不为0
,@for $n from 1 through 24
,n
从1
开始offset
默认为0
,所以@for $i from 0 through 24
,i
从0
开始
VueRow.vue
|
|
需要重构的代码
- 重复两次及以上:重复代码就是潜在的 bug,存在遗漏更新的风险
- 一眼看不懂
如何重构
- 提取变量
- 模块化
添加对齐align
属性 ⇧
VueRow.vue
|
|
- 无对齐
''
, 左对齐'left'
, 右对齐'right'
, 居中对齐'center'
- 对齐字符映射
$align-types: ('left': flex-start, ...);
scss
循环语法@each $name, $type in $align-types {...}
GridSystems.vue
|
|
实现响应式@media
⇧
根据屏幕不同的宽度预设子项不同比列,页面变化时,比列改变
|
|
- 通过传的外部数据,将不同类型响应式的属性体现在html标签上
- 外部数据类型为对象,将多个属性值写入,覆盖默认值
VueCol.vue
|
|
- 验证属性
'span', 'offset'
是否存在于moblie
中 - 改用数组实现
- 一个数组必须包含在里一个数组里
[1, 2]
∈[1, 2, 3]
- 取得对象的属性列表
keys
:Object.keys(xxx)
- 判断子集
['span', 'offset']
['span', 'offset'].includes(key)
- 判断子集
- 抽出
src/libs/objKeyValidator.ts
函数
实现验证属性的方法
@/utils/objKeyValidator.ts
|
|
VueCol.vue
|
|
CSS
不能读取JS
变量
- 需要将
JS
的变化体现在组件的:class
中 - 用
:class
的变化改变样式 - 使用不同的
CSS
类切换 - 在
@media
中设置对应不同的样式 - 在不同的客户端中,写在后面的
@media
起效时覆盖写在前面的样式,优先级更高
|
|
VueCol.vue
实现moblie
适配
|
|
VueRow.vue
|
|
- 当处于
mobile
屏幕尺寸时,设置flex-wrap: wrap;
可以换行
各屏幕尺寸参考 ⇧
xs
< 576px
- 这样的命名不够直接表示屏幕的种类,所以改为
mobile
-< 576px
pad
-577 ~ 768px
laptop
-769 ~ 992px
pc
-993 ~ 1200px
pcw
PC wide -1201 ~ 1600px
pcx
PC extreamly wide -> 1601px
VueCol.vue
实现客户端适配
|
|
重构 VueCol.vue
⇧
VueCol.vue
|
|
- 需要判断 默认样式
- 检查没有传外部数据的情况
...[]
,不能为undefined
...(mobile && [`col-mobile-${mobile.span}`, `offset-mobile-${(mobile.offset)}`] )
- 改为
...(mobile ? [`col-mobile-${mobile.span}`, `offset-mobile-${(mobile.offset)}`] : [] )
GridSystems.vue
|
|
重构重复样式 ⇧
- SCSS 语法
- 使用模块
@use "sass:math";
写在文件的开头,或紧接其他模块(math.div(24px, 24))
// 1px
@use "sass:list";
写在文件的开头,或紧接其他模块- 取值
list.nth(577px 768px, 1)
// 577px
- 取值
@each
遍历@for
循环
- 使用模块
VueCol.vue
|
|
- 双重循环
media loops
,外层@each $type, $size in $media-types {...}
,内层@for $n from 1 through 24 {...}
- 数据结构
$media-types: ($type, $size)
|
|
选择一种作为默认屏幕 ⇧
由需求决定默认的屏幕尺寸
- 将
mobile
作为默认样式,移除所有mobile
相关代码 - 设置各类屏幕匹配参数
{span, offset, mobile, pad, laptop, pc, pcw, pcx}
- 去除外部数据默认值
default: () => ({span: 12, offset: 0}),
- 匹配屏幕时启用对应的样式
...(pcx ? [`col-pcx-${pcx.span}`, `offset-pcx-${(pcx.offset)}`] : [])
,否则为空
- 去除外部数据默认值
更智能的响应式 ⇧
如果使用组件库的开发者未在
VueCol
上添加媒体查询的属性如:pad="{xxx}"
,如何兼容样式
- 原先的媒体查询是既有
min-width
又有max-width
,限定死了范围 - 只有特定档位的宽度才能匹配对应样式
- 当不写对应的属性时,就没有样式
尺寸 向小兼容 依次增大宽度
- 只写最小宽度
@media (min-width: ***px) {xxx}
注意代码顺序与样式覆盖@media (min-width: 370px) {...}
对应mobile
样式@media (min-width: 577px) {...}
对应pad
样式@media (min-width: 769px) {...}
对应laptop
样式@media (min-width: 993px) {...}
对应pc
样式@media (min-width: 1201px) {...}
对应pcw
样式@media (min-width: 1601px) {...}
对应pcx
样式
- 例如
width: 666px;
最先匹配mobile
,然后向下匹配pad
,直到没有再能匹配的宽度最终应用到pad
样式- 假设没传
:pad="{xxx}"
属性,width: 666px;
找到@media (min-width: 370px) {...}
,会自动匹配mobile
的样式
VueCol.vue
|
|
- 实现
Mobile First
响应式 移动端优先
根据内部VueRow
的子组件数量 即兄弟元素的数量来设置样式 ⇧
gutterToCol(){}
将gutter
属性值传递给子组件const {$children, gutter} = this;
$children.forEach((vm: Vue) => {}
VueRow.vue
|
|
VueGrid
组件实时监听窗口尺寸变化 ⇧
Vue.js
在监听window
上的 事件 时,往往会显得 力不从心
window.resize
比如canvas
自适应。 根据窗口的变化去变化canvas
的宽度
1.定义 一个记录宽度属性 并赋默认值 ⇧
screenWidth: document.body.clientWidth
不包括滚动条
2.方法 更新(重新赋值)this.screenWidth
⇧
|
|
3.挂载并在销毁前移除方法 ⇧
reisze
事件在created
或者mounted
的时候 去监听事件,并设置监听回调在
beforeDestroy
的时候,移除监听回调
|
|
- 注意注册监听不可用箭头函数,否则移除回调时无法引用
- 添加事件监听、移除事件监听的格式必须一致,否则会移除失效
This will:
- register your Vue method on component creation
- trigger myEventHandler when the browser window is resized
- free up memory once your component is destroyed.
参考
高内聚化
- 通过
hook
监听组件销毁钩子函数,并取消监听事件,代替 写beforeDestroy
钩子
|
|
- 在Vue组件中,可以用过
$on
或$once
去监听所有的生命周期钩子函数 - 如监听组件的
updated
钩子函数可以写成this.$on('hook:updated', () => {})
参考
4.方法改为 发布自定义事件 传递参数至父组件 ⇧
|
|
5.做一下防抖处理 ⇧
自己写的
|
|
提取为
debounce
函数
- 如果使用了
debounce
防抖 - 不要将
debounce
放到addEventListener
的方法里,直接放在处理函数里
例如:
window.addEventListener('resize', debounce(this.pageResize,200))
移除失效- 需要将
debounce()
放在this.pageResize
方法里面
./src/utils/debounce.ts
|
|
VueCol.vue
|
|
使用
lodash
- 安装
yarn add lodash
- 安装类型
yarn add -D @types/lodash
- 引用
import _ from 'lodash';
|
|
6.其他 ⇧
- 不可直接在组件上监听事件
@resize
,内部无法处理window
和document
上的事件
7.使用 Vue.js 的第三方库监听resize
事件 ⇧
1.使用库
vue-resize
添加监听组件<resize-observer @notify="handleResize" />
|
|
2.或者使用第三方封装的指令
David-Desmaisons/Vue.resize
|
|
Vue directive to detect HTML resize events based on CSS Element Queries with debouncing and throttling capacity.
3.或者使用
vue-window-size
取得width
,height
属性
参考
单元测试 ⇧
异步测试 ⇧
时机
|
|
VueRow
中的gutter
- 使用了钩子传递参数时,必须使用异步测试
业界知名UI
Bootstrap v5 Grid system
Element UI vue2 Layout 布局
Element UI vue3 Layout
Ant Design v4 Grid栅格
Bulma: the modern CSS framework that just works
参考 ⇧
相关文章 ⇧
- 无
- 作者: Joel
- 文章链接:
- 版权声明
- 非自由转载-非商用-非衍生-保持署名
- 河 掘 思 知 简