一、网格系统
定义
什么是网格系统/栅格系统
就是把一个 div 分成 N 个部分(N = 12,16,24...),每个部分无空隙或者有空隙。
在css中grid布局之前,css只有2种布局,一种是横向布局一种是纵向布局,横向布局和纵向布局嵌套组合构成很多复杂的布局
设计API
假设我们是组件的使用者,那么他会怎么使用呢?
我们不妨参考现有的比较好的组件库是怎么做的,比如ant.design
从下图可以发现,如果要做一个网格系统,我们需要做一个Row行的的组件和一个Col列的组件
默认将一个div分成24份
如果分成2列,那么左边的跨度是12.右边的跨度是12
英文中跨度对应的英文是span
我们不妨将API设置为下面这样
<g-row gutter="12">
<g-col span="12"></g-col>
<g-col span="12"></g-col>
</g-row>
<g-row gutter="12">
<g-col span="8"></g-col>
<g-col span="8"></g-col>
<g-col span="8"></g-col>
</g-row>
<g-row gutter="12">
<g-col span="6"></g-col>
<g-col span="6"></g-col>
<g-col span="6"></g-col>
<g-col span="6"></g-col>
</g-row>
<g-row gutter="12">
<g-col span="2"></g-col>
<g-col span="22"></g-col>
</g-row>
备注:
- gutter就是间隙
- span是每个栅格的跨度,和为24 即可
二、gitbranch & checkout 的使用
为什么要有gitbranch?
就是将代码按分支,保存为主分支中的一部分,如果别人只想要某一分支中的部分代码,我们只需要提供这个分支中的部分代码即可,而不是将主分支的代码全部提供
备注
- 在git中当前提交的任务叫
HEAD - 创建分支branch-1后,就有2个分支,一个是master分支,一个是branch-1分支。以前的代码就保存在branch-1分支中,现在提交的代码就提交到了master分支上了
- 老版本git的默认分支叫master现在改成main
假如现在要创建一个button-and-input分支
git branch button-and-input
如果现在要新建2个组件g-row.vue 和 g-col.vue
git add .
git commit -m 'add row and col'
git push
我们目前的分支button-and-input还在本地的分支
我们需要将这个本地分支推送到远程的分支
git push origin button-and-input:button-and-input
我们打印提交记录可以看见主分支上是有刚刚提交的记录的
但是,如果我们切换到刚刚新建的分支上时:
git checkout button-and-input
发现就没有刚刚新建的提交记录add row and col
这样,相当于将我们提交的代码按照内容的不同进行了分类
我们可以在不同的分支中查看与之相对应的代码
主要操作:
git branch xxx新建xxx分支git checkout xxx切换到xxx分支
最后git checkout main 切换到主分支,继续写代码,提交代码
三、用Vue钩子实现基本功能
实现栅格系统基本功能
我们先到index.html中实现基本的功能,再移动到对应的vue文件中
<style>
.row{
display: flex;
}
.col{
width: 50%;
height: 100px;
background: #999;
border: 1px solid red;
}
/*2/24*/
.col[data-span="2"]{
width: 8.333333%;
}
/*22/24*/
.col[data-span="22"]{
width: 91.666667%;
}
</style>
<div id="app">
<div class="row">
<div class="col"></div>
<div class="col"></div>
</div>
<div class="row">
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
</div>
<div class="row">
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
</div>
<div class="row">
<div class="col" data-span="2"></div>
<div class="col" data-span="22"></div>
</div>
写的过程中我们发现,如果想让栅格不平均分,col还需要一个支持data-span的属性,给有这个属性的col写单独的样式。css样式中我们要写24个样式并计算对应的宽度...
于是我们得用组件的写法结合scss(scss for loop)来解决这个问题,而不是用css
// g-col
<template>
<div class="col">
<slot></slot>
</div>
</template>
<script>
</script>
<style scoped lang="scss">
.col {
width: 50%;
height: 100px;
background: #999;
border: 1px solid red;
$class-prefix: col-;
//.col.col-1
//.col.col-2
@for $n from 1 through 24{
&.#{$class-prefix}#{$n}{
width: ($n / 24) * 100%;
}
}
}
</style>
备注
$class-prefix: col-是class的前缀- 从0开始的一般用i变量,否则用n
通过查看源代码发现,结果就是我们想要的
<g-row>
<g-col>1</g-col>
<g-col>2</g-col>
</g-row>
<g-row>
<g-col>1</g-col>
<g-col>2</g-col>
<g-col>3</g-col>
</g-row>
<g-row>
<g-col>1</g-col>
<g-col>2</g-col>
<g-col>3</g-col>
<g-col>4</g-col>
</g-row>
目前我们只能实现均分的栅格,实现不了不均分的栅格,比如一个占22,一个占2
因此我们要给组件一个span的属性
备注
<g-col span="2">2</g-col>: 不加冒号,2就是字符串2
<g-col :span="2">2</g-col>: 加冒号,2就是数字2
<g-col :span="'2'">2</g-col>: 或者这样表示字符串也可以(外面的双引号是html的引号,里面的单引号是js的引号)
为什么每个col加了样式width: 50%;在三列的情况下确宽度没有超出容器呢?
因为display: flex;后默认flex-wrap:no-wrap;是不换行
如果想换行可以
给row加: flex-wrap:wrap;
给col加: flex-shrink: 0;
给col添加class
<template>
<div class="col" :class="[`col-${span}`]">
<slot></slot>
</div>
</template>
<script>
export default {
name: 'g-col',
props: {
span: {
type: [Number, String]
}
}
};
</script>
<style scoped lang="scss">
.col {
width: 50%;
height: 100px;
background: #999;
border: 1px solid red;
$class-prefix: col-;
//.col.col-1
//.col.col-2
@for $n from 1 through 24 {
&.#{$class-prefix}#{$n} {
width: calc($n / 24) * 100%;
}
}
}
</style>
这样我们的网格系统就基本实现了
如果不传span,就平均分配
如果传span,就可以实现不平均分配
添加新需求:支持offset
我们应该如何让同行不同列的栅格之间有间隙?
从API的角度考虑,用户如果使用我们的组件该如何使用呢?
<g-row>
<g-col span="2">2</g-col>
<g-col span="20">20</g-col>
</g-row>
目前的组件是实现不了的,因为默认是左对齐,空白间隙直接跑到最右边去了
参考antd组件库知道,应该在col上添加offset属性
<g-row>
<g-col span="2">2</g-col>
<g-col span="20" offset="2">20</g-col>
</g-row>
接下来让我们的组件接受offset属性
<template>
<div class="col" :class="[`col-${span}`,offset && `offset-${offset}`]">
<slot></slot>
</div>
</template>
<script>
export default {
name: 'g-col',
props: {
span: {
type: [Number, String]
},
offset:{
type: [Number, String]
}
}
};
</script>
接着使用scss将offset-1 ~ offset-24 的样式全部写一次
<g-row>
<g-col>1</g-col>
<g-col>2</g-col>
</g-row>
<g-row>
<g-col>1</g-col>
<g-col>2</g-col>
<g-col>3</g-col>
</g-row>
<g-row>
<g-col>1</g-col>
<g-col>2</g-col>
<g-col>3</g-col>
<g-col>4</g-col>
</g-row>
<g-row>
<g-col span="2">2</g-col>
<g-col span="22">22</g-col>
</g-row>
<g-row>
<g-col span="2">2</g-col>
<g-col span="20" offset="2">20</g-col>
</g-row>
<g-row>
<g-col span="2">2</g-col>
<g-col span="4">4</g-col>
<g-col span="16" offset="2">16</g-col>
</g-row>
<g-row>
<g-col span="2">2</g-col>
<g-col span="4">4</g-col>
<g-col span="6" offset="2">6</g-col>
<g-col span="8" offset="2">8</g-col>
</g-row>
$class-prefix: offset-;
@for $n from 1 through 24 {
&.#{$class-prefix}#{$n} {
margin-left: calc($n / 24) * 100%;
}
}
这样我们就实现了需求,想要多少空隙就可以多少空隙,只要span和offset的值为24即可
添加新需求:即使不写offset,也有默认的空隙gutter
可不可以给col加margin?如:margin: 20px;
我们发现样式乱了,因为和我们组件在加offset时候的margin-left样式冲突了
那么改成padding呢?
我们发现页面两端是多出了2个不需要的padding:20px
如何解决这个问题呢?
我们需要给row组件的.row样式加margin: -20px;
这个空隙应该是允许用户自己设置
因此我们在g-row组件上接收一个props,属性为gutter
如果有gutter就在g-row组件上添加一个样式
<template>
<div class="row" :style="{marginLeft: -gutter/2 + 'px',marginRight: -gutter/2 + 'px'}">
<slot></slot>
</div>
</template>
刚刚在g-col组件上添加的padding是写死的,现在我们要重新写一下这个padding样式
但是,g-col怎么知道加在g-row上的gutter属性的值是多少,除非在g-col上也加gutter,然后用props接收,再写样式
<g-row gutter="20">
<g-col gutter="20">1</g-col>
<g-col gutter="20">2</g-col>
</g-row>
// g-col.vue
<template>
<div class="col" :class="[`col-${span}`,offset && `offset-${offset}`]" :style="{paddingLeft: gutter/2 + 'px',paddingRight: gutter/2 + 'px'}">
<div style="border: 2px solid green">
<slot></slot>
</div>
</div>
</template>
<script>
export default {
name: 'g-col',
props: {
span: {
type: [Number, String]
},
offset:{
type: [Number, String]
},
gutter:{
type: [Number, String]
}
}
};
</script>
这样通过在col上加padding-left: 10px; 和padding-right: 10px;
在row上加margin-left: -10px;和margin-left: -10px; 就实现了
gutter为20px的间隙
但是,我们不能让用户传到每个组件,用户只需要在g-row上加gutter属性即可,即
<g-row gutter="20">
<g-col>1</g-col>
<g-col>2</g-col>
</g-row>
那g-col怎么拿到gutter的值呢?
我们可以让g-row将gutter的值传给g-col
尝试使用vue提供的钩子函数获取g-row的子组件
created() {
console.log(this.$children);
}
为什么从外面看是空的,点进去又有呢?
因为我们大打印它的时候row没有children
打印完了,它生成了children,造成这个现象的原因是打印的时机的问题
我们应该将打印放在mounted钩子函数中
钩子函数created和mounted函数的区别是什么?
我们不妨拿使用原生的js生成div做类比
var div = document.createElement('div') // created
document.body.appendChild(div) // mounted
created只是在内存中生成了对象或者说组件(组件也是对象)
mounted就是把对象挂到页面中去
如果div还有子div,顺序应该是
var div = document.createElement('div') // created
var childDiv = document.createElement('div') // child created
div.appendChild(childDiv) // child mounted
document.body.appendChild(div) // mounted
看下顺序
- 创建父div
- 创建子div
- 把子div添加到父div身上
- 把父div添加到body
在这里先创建div还是先创建childDiv的顺序是无所谓的
但是类比到vue中,是先创建子组件还是父组件呢?这时候vue是要做选择的,我们怎么知道vue是怎么选择的呢?
我们只需要通过简单的log就可以知道,不需要看源代码
在g-row组件和g-col组件中打印
created() {
console.log('col created');
},
mounted() {
console.log('col mounted');
}
created() {
console.log('row created');
},
mounted() {
console.log('row mounted');
}
我们看见了顺序
- 创建一个row(这里是父组件)
- 创建两个col(这里是子组件)
- 将2个col挂载到row上
- row挂载到页面上
这就是vue中父子组件中钩子函数的触发顺序
从上图可以知道,当执行row mounted之后所有子子孙孙都已经挂载到row身上了,这时在g-row的mounted钩子函数中已经可以取到它们了
因此,我们就可以在父组件g-row的mounted钩子函数中获取到它的子组件,给它们添加gutter属性
看下完整的代码
// g-row
<template>
<div class="row" :style="{marginLeft: -gutter/2 + 'px',marginRight: -gutter/2 + 'px'}">
<slot></slot>
</div>
</template>
<script>
export default {
name: 'g-row',
props:{
gutter:{
type: [Number,String]
}
},
mounted() {
this.$children.forEach((vm)=>{
vm.gutter = this.gutter
})
}
}
</script>
<style scoped lang="scss">
.row{
display: flex;
}
</style>
// g-col
<template>
<div class="col" :class="[span &&`col-${span}`,offset && `offset-${offset}`]" :style="{paddingLeft: gutter/2 + 'px',paddingRight: gutter/2 + 'px'}">
<div style="border: 2px solid green">
<slot></slot>
</div>
</div>
</template>
<script>
export default {
name: 'g-col',
props: {
span: {
type: [Number, String]
},
offset:{
type: [Number, String]
},
},
data(){
return{
gutter:0
}
},
};
</script>
<style scoped lang="scss">
.col {
width: 50%;
height: 100px;
$class-prefix: col-;
//.col.col-1
//.col.col-2
@for $n from 1 through 24 {
&.#{$class-prefix}#{$n} {
width: calc($n / 24) * 100%;
}
}
$class-prefix: offset-;
@for $n from 1 through 24 {
&.#{$class-prefix}#{$n} {
margin-left: calc($n / 24) * 100%;
}
}
}
</style>
四、重构和重写
定义
重构: 通过微小的调整让我们的代码变得更好,并且是每天持续的做这细微的调整。重构是每天要做的事,只要完成一个小功能,就要对代码进行重构。不要相信,等有空了再进行重构,重构是每天都在做的事
重写: 大的调整,只要是隔一段时间去写就是重写,因为已经积累了很多bug。如果是完成了100个功能,再对代码进行重构,那就不是重构,一定是重写。隔一段时间都忘了当初写的时候的具体功能,还怎么重构,一定是重写
什么代码需要重构
- 重复2次及以上的代码
比如在A文件写了一段代码,在B文件写了一样的代码,如果你发现B文件的这段代码有bug,于是就改了,但是你忘了在A文件也写了一样的代码,但是你忘了去修改。也就是说一旦2个地方有一样的代码,改了一个地方,忘了改另外一个地方,那么这段代码就是潜在的bug
如果存在重复的代码,应该将这段代码写在另外一个文件并导出函数,然后在A文件和B文件引入这个函数,如果函数需要更新就直接更新这个函数就可以了,A文件和B文件引入的都是这个函数,不会存在潜在的bug
如果不改,那么就会因为需求的变更,而变成 1/2/、1/3、1/4的bug
- 一眼看不懂的代码
比如:下面组件g-col中的一段代码
这段代码在理解上本身没有什么难度,但是代码太长了,读起来就不想读,如果属性再多,就会读起来麻烦,而且也很丑,原因是排版太复杂了
这时候需要减轻眼睛的负担,让用户一眼就知道我们在干什么
我们不妨加一个中间变量,比如:style的值就是一个对象,为什么不把这个对象放到data里呢?
有的时候重构并没有什么逻辑上的优化,只是为了让用户一眼就能看出对象的结构是什么
重构也是有风险的,有时候重构完发现出问题了,我们发现styley样式并没有成功添加,为什么呢?
因为gutter是在mounted之后赋值的,而colStyle写在data里,只会在created的时候读一次,后面就不会再读它的值了
如果后面变量gutter的值变了,colStyle是不会随着发生变化的
如果想让gutter一改变,colStyle跟着改变,我们应该使用计算属性computed
这就是计算属性的应用场景 :如果一个属性的值是要随着另外一个属性值的变化而变化的,那么不应该将这个属性的值放到data里面,而是应该放到计算属性里
接着把class也重构一下,它也是计算属性
<template>
<div class="col" :class="colClass"
:style="colStyle">
<slot></slot>
</div>
</template>
<script>
export default {
name: 'g-col',
props: {
span: {
type: [Number, String]
},
offset: {
type: [Number, String]
},
},
data() {
return {
gutter: 0
};
},
computed: {
colClass(){
let {span,offset} = this;
return [
span &&`col-${span}`,
offset && `offset-${offset}`
]
},
colStyle() {
return {
paddingLeft: this.gutter / 2 + 'px',
paddingRight: this.gutter / 2 + 'px',
};
}
}
};
</script>
五、手动测试
添加新需求
让栅格支持左对齐、居中对齐、右对齐
// index.html
<style>
.demoBox {min-height: 50px;background: #e7e7e7;border: 1px solid cornflowerblue;}
.logo-wrapper {
padding: 10px;
}
</style>
<div id="app">
<g-row class="topbar">
<g-col class="demoBox" span="9">
<g-row align="left">
<g-col>1</g-col>
<g-col>2</g-col>
<g-col>3</g-col>
<g-col>4</g-col>
</g-row>
</g-col>
<g-col class="demoBox" span="15">
<g-row align="right">
<g-col>1</g-col>
<g-col>2</g-col>
<g-col>3</g-col>
<g-col>4</g-col>
<g-col>5</g-col>
<g-col>6</g-col>
<g-col>7</g-col>
</g-row>
</g-col>
</g-row>
<g-row class="logo-and-search-and-qrcode">
<g-col class="demoBox" span="4">
<g-row align="center">
<g-col>
<div class="logo-wrapper">
<img width="50" class="logo"
src="https://img.alicdn.com/imgextra/i1/O1CN01BqHq2Z28Ns0s0KLHa_!!6000000007921-2-tps-126-54.png">
</div>
</g-col>
</g-row>
</g-col>
<g-col class="demoBox" span="14"></g-col>
<g-col class="demoBox" span="6"></g-col>
</g-row>
<hr>
</html>
// g-row组件
<template>
<div class="row" :style="rowStyle" :class="rowClass">
<slot></slot>
</div>
</template>
<script>
export default {
name: 'g-row',
props:{
gutter:{
type: [Number,String]
},
align:{
type: String,
validator(value){
return ['left','center','right'].includes(value)
}
}
},
computed:{
rowStyle(){
let {gutter} = this
return {marginLeft: -gutter/2 + 'px',marginRight: -gutter/2 + 'px'}
},
rowClass(){
let {align} = this
return [align && `align-${align}`]
}
},
mounted() {
this.$children.forEach((vm)=>{
vm.gutter = this.gutter
})
}
}
</script>
<style scoped lang="scss">
.row{
display: flex;
&.align-left {
justify-content: flex-start;
}
&.align-center {
justify-content: center;
}
&.align-right {
justify-content: flex-end;
}
}
</style>
思路:
- props接收
align属性值为left center right - 添加样式class,class名为
align + ${align} - style中定义这个样式
备注
- 使用
g-row和g-col嵌套使用也可以实现居中 - 理论上使用
g-row和g-col嵌套的方式可以实现所有的布局
六、实现响应式
灵活? 限制?
我们的组件只能实现简单的响应式,仅仅是做到宽度和相对位置的改变,因为我们目前的栅格系统已经基本可以实现所有的布局,只要按照规定给响应的属性即可
灵活是设计师想要的需求,限制是工程师想要的需求。
工程师永远想限制设计师,但是设计师永远不想被限制,如果我们是工程师,我们一定要限制设计师,这样工作量就会大幅度下降,因为不需要给特殊风格的页面单独写样式,只用复用组件即可
但是在灵活和限制之间我们也要做权衡
比如:要做一个后台管理系统,就可以使用grid布局,自己内部人看,无所谓。但是,如果要做活动页就不能使用grid布局
响应式
API如何设计
Antd中实现了响应式,我们可以参考它的API设计
我们要实现的响应式要达到在不同设备尺寸下,页面元素的大小比例以及位置发生变化
<g-row>
<g-col span="4" phone-span="12" offset="1" phone-offset="2"></g-col>
<g-col span="20" phone-span="12"></g-col>
</g-row>
站在使用组件者的角度可能会这么使用响应式
默认情况下是 4 20
但是在手机设备下就变成 12 12
但是这样做的问题是:如果组件上使用了很多属性,那么每次都要默认情况下写一次,不同设备下再写一次
可不可以简化成下面这样,让这个属性接受一个对象,它里面的属性的值会覆盖默认的属性的值
<g-row>
<g-col span="4" offset="1" :phone="{span:12,offset:2}"></g-col>
<g-col span="20" phone-span="12"></g-col>
</g-row>
实现
// index.html
<g-row gutter="20">
<g-col span="2" :phone="{span:24}" :ipad="{span:8}" :narrow-pc="{span:4}" :pc="{span:2}" :wide-pc="{span:1}">2</g-col>
<g-col span="22" :phone="{span:24}" :ipad="{span:16}" :narrow-pc="{span:20}" :pc="{span:22}" :wide-pc="{span:23}">22</g-col>
</g-row>
// g-col组件
<template>
<div class="col" :class="colClass"
:style="colStyle">
<slot></slot>
</div>
</template>
<script>
let validator = (value) => {
let keys = Object.keys(value)
let valid = true
keys.forEach( key => {
if(!['span','offset'].includes(key)){
valid = false
}
})
return valid
}
export default {
name: 'g-col',
props: {
span: {type: [Number, String]},
offset: {type: [Number, String]},
phone:{type: Object, validator},
ipad:{type: Object, validator},
narrowPc:{type: Object, validator},
pc:{type: Object, validator},
widePc:{type: Object, validator}
},
data() {
return {
gutter: 0
};
},
computed: {
colClass(){
let {span,offset,phone,ipad,narrowPc,pc,widePc} = this;
return [
span &&`col-${span}`,
offset && `offset-${offset}`,
...(phone && [`col-phone-${phone.span}`]),
...(ipad && [`col-ipad-${ipad.span}`]),
...(narrowPc && [`col-narrowPc-${narrowPc.span}`]),
...(pc && [`col-pc-${pc.span}`]),
...(widePc && [`col-widePc-${widePc.span}`]),
]
},
colStyle() {
return {
paddingLeft: this.gutter / 2 + 'px',
paddingRight: this.gutter / 2 + 'px',
};
}
}
};
</script>
<style scoped lang="scss">
.col {
width: 50%;
min-height: 80px;
$class-prefix: col-;
//.col.col-1
//.col.col-2
@for $n from 1 through 24 {
&.#{$class-prefix}#{$n} {
width: calc($n / 24) * 100%;
}
}
$class-prefix: offset-;
@for $n from 1 through 24 {
&.#{$class-prefix}#{$n} {
margin-left: calc($n / 24) * 100%;
}
}
//手机端
@media (max-width: 576px) {
$class-prefix: col-phone-;
@for $n from 1 through 24 {
&.#{$class-prefix}#{$n} {
width: calc($n / 24) * 100%;
}
}
$class-prefix: offset-phone-;
@for $n from 1 through 24 {
&.#{$class-prefix}#{$n} {
margin-left: calc($n / 24) * 100%;
}
}
}
//ipad端
@media (min-width: 577px) and (max-width: 768px) {
$class-prefix: col-ipad-;
@for $n from 1 through 24 {
&.#{$class-prefix}#{$n} {
width: calc($n / 24) * 100%;
}
}
$class-prefix: offset-ipad-;
@for $n from 1 through 24 {
&.#{$class-prefix}#{$n} {
margin-left: calc($n / 24) * 100%;
}
}
}
//窄屏幕
@media (min-width: 769px) and (max-width: 992px) {
$class-prefix: col-narrow-pc-;
@for $n from 1 through 24 {
&.#{$class-prefix}#{$n} {
width: calc($n / 24) * 100%;
}
}
$class-prefix: offset-narrow-pc-;
@for $n from 1 through 24 {
&.#{$class-prefix}#{$n} {
margin-left: calc($n / 24) * 100%;
}
}
}
//PC
@media (min-width: 993px) and (max-width: 1200px) {
$class-prefix: col-pc-;
@for $n from 1 through 24 {
&.#{$class-prefix}#{$n} {
width: calc($n / 24) * 100%;
}
}
$class-prefix: offset-pc-;
@for $n from 1 through 24 {
&.#{$class-prefix}#{$n} {
margin-left: calc($n / 24) * 100%;
}
}
}
// 宽PC
@media (min-width: 1201px){
$class-prefix: col-wide-pc-;
@for $n from 1 through 24 {
&.#{$class-prefix}#{$n} {
width: calc($n / 24) * 100%;
}
}
$class-prefix: offset-wide-pc-;
@for $n from 1 through 24 {
&.#{$class-prefix}#{$n} {
margin-left: calc($n / 24) * 100%;
}
}
}
}
</style>
备注:
- phone的类型是对象,同时它的属性只能包含
span 和 offset - 把传给组件的phone属性体现在html标签上并以class的方式展示出来,class一发生变化,style就产生相应的变化,我们得让
g-col组件再pc端和phone端有不同的class - 同时要使用@media划分屏幕宽度的几个范围段,针对不同的范围段设置样式,同时由于@media媒体查询中的css写在下面,因此当它触发时,会覆盖上面的样式
- antd不同的屏幕尺寸叫
xs sm md lg xl xxl从名字上我们还是无法知道当前设备是什么设备还不如叫phone ipad - 如果想让栅格变成上下结构,就让span都占24份,同时
g-row中flex-wrap:wrap:设置为允许换行,如上图所示当在phone屏幕下就变成了上下布局了
<g-row gutter="20">
<g-col span="2" :phone="{span:24}">2</g-col>
<g-col span="22" :phone="{span:24}">22</g-col>
</g-row>
目前还存在一个问题:如果五种设备的样式都写了,那么span=2就没有意义了
<g-col span="2" :phone="{span:24}" :ipad="{span:8}" :narrow-pc="{span:4}" :pc="{span:2}" :wide-pc="{span:1}">2</g-col>
因此我们要选择一种,作为我们的默认样式。假如我们的组件定位就是以手机用户为主,那么就把phone作为默认,也就不要再传phone了,把和phone相关的代码删除即可
如果传的时候没传ipad和narrow-pc,但是当前的屏幕尺寸在ipad上,那么样式是使用ipad的样式?还是使用narrow-pc的样式?
这还是要根据需求来定
如果你觉得你面向的是手机的用户,那么如果不属于手机设备,都使用手机设备的样式
或者就近使用样式,这就需要我们做出权衡
我们不妨让响应式的规则往下走,往下走的意思是如如果传了phone和pc,当前的设备是ipad,但是没传ipad属性,那么就走pc的样式(phone -> ipad -> narrow pc -> pc -> wide pc)
七、完善测试用例
解决没有传ipad但是屏幕尺寸是ipad的问题
我们不应该将媒体查询的规则写的那么死板,我们可不可以只写一个最低值或者一个最、最高值
//屏幕宽度 = width
min-width > 0 span = 12 phone
min-width > 576 span = 10 ipad
min-width >768 span = 6 narrow-pc
min-width >992 span = 4 pc
min-width >1200 span = 2 wide-pc
也就是说如果屏幕宽度落在phone和ipad,那么按照向下走的原则,就使用ipad的样式
如果没写传ipad,但是屏幕尺寸是ipad,那么它的尺寸也在iphone尺寸的区间,那么就使用phone的样式
回到代码中我们就要调整一下媒体查询样式,自上而下按照phone -> ipad -> narrow pc -> pc -> wide pc的顺序
同时尺寸的范围只要给一个起点范围就行了,不需要区间
<g-row>
<g-col span="24" :narrow-pc="{span: 8}">
<div class="demo" ></div>
</g-col>
<g-col span="24" :narrow-pc="{span: 8}">
<div class="demo" ></div>
</g-col>
<g-col span="24" :narrow-pc="{span: 8}">
<div class="demo" ></div>
</g-col>
<g-col span="24" :narrow-pc="{span: 8}">
<div class="demo" ></div>
</g-col>
<g-col span="24" :narrow-pc="{span: 8}">
<div class="demo" ></div>
</g-col>
<g-col span="24" :narrow-pc="{span: 8}">
<div class="demo" ></div>
</g-col>
</g-row>
<style scoped lang="scss">
.col {
width: 50%;
min-height: 80px;
$class-prefix: col-;
//.col.col-1
//.col.col-2
//默认是phone
@for $n from 1 through 24 {
&.#{$class-prefix}#{$n} {
width: calc($n / 24) * 100%;
}
}
$class-prefix: offset-;
@for $n from 1 through 24 {
&.#{$class-prefix}#{$n} {
margin-left: calc($n / 24) * 100%;
}
}
//ipad端
@media (min-width: 577px) {
$class-prefix: col-ipad-;
@for $n from 1 through 24 {
&.#{$class-prefix}#{$n} {
width: calc($n / 24) * 100%;
}
}
$class-prefix: offset-ipad-;
@for $n from 1 through 24 {
&.#{$class-prefix}#{$n} {
margin-left: calc($n / 24) * 100%;
}
}
}
//窄屏幕
@media (min-width: 769px){
$class-prefix: col-narrow-pc-;
@for $n from 1 through 24 {
&.#{$class-prefix}#{$n} {
width: calc($n / 24) * 100%;
}
}
$class-prefix: offset-narrow-pc-;
@for $n from 1 through 24 {
&.#{$class-prefix}#{$n} {
margin-left: calc($n / 24) * 100%;
}
}
}
//PC
@media (min-width: 993px){
$class-prefix: col-pc-;
@for $n from 1 through 24 {
&.#{$class-prefix}#{$n} {
width: calc($n / 24) * 100%;
}
}
$class-prefix: offset-pc-;
@for $n from 1 through 24 {
&.#{$class-prefix}#{$n} {
margin-left: calc($n / 24) * 100%;
}
}
}
// 宽PC
@media (min-width: 1201px){
$class-prefix: col-wide-pc-;
@for $n from 1 through 24 {
&.#{$class-prefix}#{$n} {
width: calc($n / 24) * 100%;
}
}
$class-prefix: offset-wide-pc-;
@for $n from 1 through 24 {
&.#{$class-prefix}#{$n} {
margin-left: calc($n / 24) * 100%;
}
}
}
}
</style>
这样就达到了我们想要的效果,默认是phone手机,当尺寸在ipad,但是又没有写phone的属性的时候,就使用phone的样式,当屏幕尺寸在窄屏幕就又使用了窄屏幕的样式narrow-pc,这种适配的策略叫Mobile First,中文叫移动端优先
重构下代码
优化前
// 往数组中添加class
colClass(){
let {span,offset,ipad,narrowPc,pc,widePc} = this;
return [
span &&`col-${span}`,
offset && `offset-${offset}`,
...(ipad ? [`col-ipad-${ipad.span}`]:[]),
...(narrowPc ?[`col-narrow-pc-${narrowPc.span}`]:[]),
...(pc ? [`col-pc-${pc.span}`]:[]),
...(widePc ? [`col-wide-pc-${widePc.span}`]:[]),
]
},
优化后
methods: {
createClasses(obj, str = '') {
if (!obj) {
return [];
}
let array = [];
if (obj.span) array.push(`col-${str}${obj.span}`);
if (obj.offset) array.push(`offset-${str}${obj.offset}`);
return array;
}
},
computed: {
colClass() {
let {span, offset, ipad, narrowPc, pc, widePc} = this;
let createClasses = this.createClasses
return [
...createClasses({span, offset}),
...createClasses(ipad, 'ipad-'),
...createClasses(narrowPc, 'narrow-pc-'),
...createClasses(pc, 'pc-'),
...createClasses(widePc, 'wide-pc-')
];
},
colStyle() {
return {
paddingLeft: this.gutter / 2 + 'px',
paddingRight: this.gutter / 2 + 'px',
};
}
}
};
总结
目前组件完成的功能有:
- 24列栅格布局
- 可以加空隙gutter
- 可以设置offset
- 可以用span指定占的列数
- 它是响应式的
添加测试用例
每个组件要写单独的测试文件
新建 test/col.test.js 和 test/row.test.js
测g-row组件
我们该如何测试g-row.vue文件呢?
我们知道这个组件接收2个参数,一个是gutter,另外一个是align
it('接收 gutter 属性', () => {
Vue.component('g-row', Row);
Vue.component('g-col', Col);
const div = document.createElement('div');
document.body.appendChild(div)
div.innerHTML = `
<g-row gutter="20">
<g-col span="12"></g-col>
<g-col span="12"></g-col>
</g-row>
`
const vm = new Vue({
el: div
})
console.log(vm.$el.outerHTML)
});
打印的时候padding为0,但是并不代表它真的padding为0
只是我们打印的时候是0,后来padding的值变了,变的过程是异步的过程
备注
- vue中不支持将Row的实力直接挂载到body上,因此我们要创建一个div,将Row的实例挂载到div上,最后将div挂载到body上
- 当前的row中没有col因此要将Col引入进来一起测
- 那么如何动态的往row里添加col,然后才将row挂载到div上?我们只知道html形式的将rol作为row的子组件,暂时不知道如何通过JS将一个组件放到另外一个组件中,因此我们构建html形式的往row里添加col
- 打印的时候没有并不代表它真的没有,如:数组a一开始是空数组,当push数据后,从外面看还是空数组,但是点进去里面是有数据的
因此,我们只能进行异步的测试,为什么要异步的进行测试?
这就需要我们了解vue的渲染过程
vue的渲染过程
看下普通的DOM的渲染过程:
var div = document.createElement('div')
var child = document.createElement('div')
div.appendChild(child)
document.body.appendChild(div)
这是一个同步的过程
但是在vue中不是这样,vue中子组件放到父组件不是同步的(可能是基于性能的考虑,如果是同步的可能要等结果等很久),或者说钩子的调用不是同步的
在vue中组件嵌套子组件,类比div.appendChild(child)就会触发child组件的mounted钩子函数
那么mounted钩子函数中的代码是同步的触发还是异步的触发?
如果mounted钩子函数中有很多代码,并且涉及到很重的运算,如果是同步的触发,vue就要一直等mounted的结果,性能就会很差,因此是异步的
vue不能一直等上面的mounted钩子函数的结果,因此它要先去执行其他的代码,如:document.body.appendChild(div)此时又会触发div的mounted钩子
因此队列中就有2个mountend队列
如果此时测试用例中console.log(div.outerHTML)
那么结果是先执行console.log(div.outerHTML)还是先执行队列中的mounted钩子函数呢?
由于console.log(div.outerHTML)是同步代码,因此是先执行的
再回过头看下测试用例中的代码
it('接收 gutter 属性', () => {
Vue.component('g-row', Row);
Vue.component('g-col', Col);
const div = document.createElement('div');
document.body.appendChild(div)
div.innerHTML = `
<g-row gutter="20">
<g-col span="12"></g-col>
<g-col span="12"></g-col>
</g-row>
`
const vm = new Vue({
el: div
})
console.log(vm.$el.outerHTML)
});
我们在加了gutter属性,会认为g-col组件上会有10px的padding
但是我们g-col组件的10px的padding样式是在g-row组件的moutend钩子函数里去改的
在我们console.log(vm.$el.outerHTML)之前异步的mounted还没有执行完,因此我们打印的结果中g-col组件中还没有样式
因此我们需要异步的调用console.log(vm.$el.outerHTML),给它加一个定时器
setTimeout(()=>console.log(vm.$el.outerHTML) ,0)
这样做的结果就是会将上面的代码放到任务队列的第三项
等前2项的异步结果(child mounted 和 div mounted)执行完成之后再执行console.log(vm.$el.outerHTML)
it('接收 gutter 属性', (done) => {
Vue.component('g-row', Row);
Vue.component('g-col', Col);
const div = document.createElement('div');
document.body.appendChild(div)
div.innerHTML = `
<g-row gutter="20">
<g-col span="12"></g-col>
<g-col span="12"></g-col>
</g-row>
`
const vm = new Vue({
el: div
})
setTimeout(()=>{
const row = vm.$el.querySelector('.row')
expect(getComputedStyle(row).marginLeft).to.eq('-10px')
expect(getComputedStyle(row).marginRight).to.eq('-10px')
const cols = vm.$el.querySelectorAll('.col')
expect(getComputedStyle(cols[0]).paddingRight).to.eq('10px')
expect(getComputedStyle(cols[1]).paddingLeft).to.eq('10px')
done()
vm.$el.remove()
vm.$destroy()
} ,1000)
});
it('接收 align 属性',()=>{
const div = document.createElement('div')
document.body.appendChild(div)
const Constructor = Vue.extend(Row)
const vm = new Constructor({
propsData:{
align: 'center'
}
}).$mount(div)
const element = vm.$el
expect(getComputedStyle(element).justifyContent).to.equal('center')
div.remove()
vm.$destroy()
})
备注
- 如果不调用done,默认测试用例中的代码都是同步代码,因此setTimeout中的代码就不会执行
- 也就是说如果测试用例里面存在任何的异步,都需要在参数中加个done,并在异步任务完成后调用一下done,通知它我们的测试完成
- 由于测试的时候karma默认会帮我们打开浏览器进行测试,因此每次我们测试完成后最好删除在页面中创建的元素,否则有可能会干扰下一次的测试
总结
- 再想一想测试用例中造的结构,就是组件中嵌套组件,使用html模板的方式
- 想一想为什么一开始
console.log(vm.$el.outerHTML)为什么打印不出col的padding为10px的样式,原因是需要异步 - 如果测试用例中用到异步,异步结束之后要调用一下
done - 如果在vue中用到了钩子函数就会牵扯到异步代码
测g-row组件
import {doc} from 'mocha/lib/reporters';
const expect = chai.expect;
import Vue from 'vue';
import Row from '../src/g-row.vue';
import Col from '../src/g-col.vue';
Vue.config.productionTip = false;
Vue.config.devtools = false;
describe('Col', () => {
it('存在', () => {
expect(Col).to.exist;
});
it('接收 span 属性',()=>{
const div = document.createElement('div')
document.body.appendChild(div)
const Constructor = Vue.extend(Col)
const vm = new Constructor({
propsData:{
span: 1
}
}).$mount(div)
console.log(vm.$el)
expect(vm.$el.classList.contains('col-1')).to.equal(true)
div.remove()
vm.$destroy()
})
it('接收 offset 属性',()=>{
const div = document.createElement('div')
document.body.appendChild(div)
const Constructor = Vue.extend(Col)
const vm = new Constructor({
propsData:{
offset: 1
}
}).$mount(div)
expect(vm.$el.classList.contains('offset-1')).to.equal(true)
div.remove()
vm.$destroy()
})
it('接收 ipad 属性',()=>{
const div = document.createElement('div')
document.body.appendChild(div)
const Constructor = Vue.extend(Col)
const vm = new Constructor({
propsData:{
ipad: {span:1,offset:2}
}
}).$mount(div)
expect(vm.$el.classList.contains('col-ipad-1')).to.equal(true)
expect(vm.$el.classList.contains('offset-ipad-2')).to.equal(true)
div.remove()
vm.$destroy()
})
it('接收 narrowPc 属性',()=>{
const div = document.createElement('div')
document.body.appendChild(div)
const Constructor = Vue.extend(Col)
const vm = new Constructor({
propsData:{
narrowPc: {span: 4,offset: 1}
}
}).$mount(div)
expect(vm.$el.classList.contains('col-narrow-pc-4')).to.equal(true)
expect(vm.$el.classList.contains('offset-narrow-pc-1')).to.equal(true)
div.remove()
vm.$destroy()
})
it('接收 pc 属性',()=>{
const div = document.createElement('div')
document.body.appendChild(div)
const Constructor = Vue.extend(Col)
const vm = new Constructor({
propsData:{
pc: {span:6,offset:2}
}
}).$mount(div)
expect(vm.$el.classList.contains('col-pc-6')).to.equal(true)
expect(vm.$el.classList.contains('offset-pc-2')).to.equal(true)
div.remove()
vm.$destroy()
})
it('接收 widePc 属性',()=>{
const div = document.createElement('div')
document.body.appendChild(div)
const Constructor = Vue.extend(Col)
const vm = new Constructor({
propsData:{
widePc: {span:8,offset:4}
}
}).$mount(div)
console.log(vm.$el)
expect(vm.$el.classList.contains('col-wide-pc-8')).to.equal(true)
expect(vm.$el.classList.contains('offset-wide-pc-4')).to.equal(true)
div.remove()
vm.$destroy()
})
});
备注
- 测试css一般只要expect它有没有某个class
八、默认布局
Ant.design中的默认布局及API
基本上所有的网站都可以使用这四种布局中的一种
因此,我们可以把常用的布局做成组件
Tips: Amend本次提交合并到上次提交
明确需求
如果是用户会怎么使用布局组件?
<g-layout>
<g-header></g-header>
<g-content></g-content>
<g-footer></g-footer>
</g-layout>
<g-layout>
<g-header></g-header>
<g-layout>
<g-slider></g-slider>
<g-content></g-content>
</g-layout>
<g-footer></g-footer>
</g-layout>
<g-layout>
<g-header></g-header>
<g-layout>
<g-content></g-content>
<g-slider></g-slider>
</g-layout>
<g-footer></g-footer>
</g-layout>
<g-layout>
<g-slider></g-slider>
<g-layout>
<g-header></g-header>
<g-content></g-content>
<g-footer></g-footer>
</g-layout>
</g-layout>
备注
- layout标签就是没有意义的div标签
布局一
需求
实现
这里我们可以使用git branch grid将之前的代码保存到新的分支,这样就像主干分了一支分支
接下来可以继续提交代码到主分支中
新建 g-header g-content g-side g-footer g-layout组件
在app.js全局注册这些组件
// g-layout组件
.layout{
display: flex;
flex-direction: column;
border: 1px solid red;
}
// g-content组件
.content{
flex-grow: 1;
}
布局二
目前的layout是垂直布局的,但是中间的layout需要横向布局,难道要加一个额外的属性?
我们不参考下ant.design是怎么实现的
我们发现它只要发现g-layout组件下有sider,就给layout添加一个ant-layout-has-sider类,在这个样式中将flex布局的方向变成fiex-direction:row;
因此我们也可以按照这个思路来做
我们要在g-layout组件中借助回调钩子函数mounted,发现子组件(相当于DOM中子元素的概念)里有sider之后去加class
在vue中是先创建父组件,再创建子组件,将子组件添加到父组件中,最后再将父组件添加到页面中,因此我们应该要在mounted钩子函数中去取,如果是在created钩子函数去取,时机就不对,此时子组件还没有放到父组件中
单文件组件name的第一个作用是使用vue的插件,在开发者工具里可以清晰的知道当前页面中组件的组织结构。
第二个作用是只有单文件组件写了name,我们才能知道g-layout组件下有没有sider组件
// g-layout组件
mounted() {
console.log(this.$children);
}
export default {
name: 'g-layout',
mounted() {
this.$children.forEach(vm=>{
console.log(vm.$options.name);
})
}
};
从上图可以看出第一个g-layout组件的直接子组件有g-header g-layout g-footer,第二个g-layout组件的直接子组件有g-sider 和 g-content
这样我们就知道g-layout组件下有g-layout组件,我们就可以添加class了
mounted() {
this.$children.forEach(vm=>{
if(vm.$options.name === 'g-sider'){
this.layoutClass.hasSider = true
};
})
}
了解了一个套路之后,后面都可以经常用到这个套路:g-layout组件mounted的时候看它的$children子组件有没有一个叫g-sider,如果有就给g-layout组件添加一个hasSider的class(速记:mounted之后看儿子)
后面添加.hasSider样式即可
布局三
布局三就是把布局二中的Content和Sider换一下顺序
布局四
只要给g-layout组件添加一个flex-grow: 1;即可