搞轮子:从业务到组件Tabs

前言

因为之前为 UI 库由我来做且维护,但缺点是集成在项目中,不便于其他项目使用,且组件被同事改了之后会导致之前沿用的组件乱掉。所以 UI 由独立库维护是最好的。正逢项目不急,就将其排上了日期,其中写了

皮肤概念解决的是 UI 库目前适用于两个主色调、次色调不同的项目

css 定位解决的是 Tag 组件的 border 边框让尺寸变长问题

现在要提取 Tab 组件,发现原来的组件不太合理

业务中的 Tab 组件

业务效果如图所示,滑动它,能连带着切换下划线和内容

image-20211115163300514

后端返回数据结构

总结构:

image-20211115163114381

详细结构:

image-20211115163050904

当时的写法:

<Tab
  ref={tabRef}
  data={tagGroupProductList}
  sticky={true}
  selected={selectedTab}
  onChange={onHandleChangeTab}
  onClick={onClickToBtn}
/>

组件 Tab 分为三部分,主 Tab、TabItem、TabContext

image-20211115163152899

在 Tab 中,集成了 TabItem 和 TabContext

TabItem:头部图片+文字

TabContext:内容卡片,支持滑动

// 伪代码,此为 Tab
<div className="jing-tab" style={style} ref={ref}>
  // 遍历<TabItem></TabItem>
  // 塞入当前下标页的数据,展示 TabContext <TabContext />
</div>

又提供滑动后的回调 changeSwiper。当滑动后,意味着组件头部的位置改变,也意味着 context 数据的改变。虽然写的有点饶,但我想表达的是原来的组件的业务和逻辑绑定密切

这种写法是封装好了样式,只要传 props 即可,相当于木偶组件,使用者传入数据即可。

<Tab
  ref={tabRef}
  data={tagGroupProductList} // 传入一次性参数
  sticky={true}
  selected={selectedTab}
  onChange={onHandleChangeTab}
  onClick={onClickToBtn}
/>

缺点是子组件不能传参数,你不能控制子组件,当遇到类似的 Tab(标签页)效果,但内容结构不一致就会导致不可用。说白了,这个 Tab 只适用于当前页面的效果,没有普世性

为了组件的可拓展性,业务和项目尽量分开

重构组件

我一开始的设想是希望做成这样:

<Tabs swiper={true} active="0">
  <Tab title="标签1">内容1</Tab>
  <Tab title="标签2">内容2</Tab>
  <Tab title="标签3">内容3</Tab>
</Tabs>

Tabs 控制标签页是否能滑动,能否点击,滑动到那个标签页等等,Tab 控制当前标签的标题和内容

但这样,Tabs 和 Tab 就分离成了两个组件,有必要分离吗?用到标签页(Tabs)的时候必然会用 Tab,所以我后面改造成了

<Tabs value="{value}">
  <Tabs.Panel title="标签1">内容 1</Tabs.Panel>
  <Tabs.Panel title="标签2">内容 2</Tabs.Panel>
  <Tabs.Panel title="标签3">内容 3</Tabs.Panel>
</Tabs>

Tab 嫁接在 Tabs 的子组件中

思路与做法

Tabs 的作用是为了外界的 props

Tab 的作用是展示数据,如 title 和 content

思路有了,怎么做?

Tabs 的属性包括但不限于:

  • value:绑定当前选中标签的标识符

  • swipeable:是否开启手势滑动切换

  • sticky:是否使用粘性定位布局

  • disabled:是否禁用标签

  • swipeThreshold:滚动阈值

拆开了 Tabs 和 Tab 的结构是怎么样的

const Panel = (props) => {
    const classes = classnames(prefixCls, props.className, {
        [`${prefixCls}--active`]: !!props.selected,
        [`${prefixCls}--disabled`]: !!props.disabled,
    });
    return (
        <div className="jing-tabs__panel" style={props.style} role="tabpanel">
            {props.children}
        </div>
    )
}

const Tabs = (props) => {
  return (
    <div className="jing-tabs">
      <div className="jing-tabs__wrap">
          React.Children.map(children, (item: any, index: number) => {
            // 简化
          return (
             <Title ...item.props/>
          );
    })
      </div>
      <div className="jing-tabs__content">
         // 遍历 React.Children,将Children中的数据塞入 TabPanel 中
          React.Children.map(children, (item,index) => (
           <TabPanel {...item.props}/>
          ))
      </div>
    </div>
  )
}

Tabs.Panel = Panel

这里有个知识点:React.Children

什么是 React.Children?

React.Children 提供了用于处理 this.props.children 不透明数据结构的实用方法。

React.Children.map 的使用方法

React.Children.map(children, function[(thisArg)])

所以在代码中

React.Children.map(children, (item, index) => <TabPanel {...item.props} />);

(item, index) => <TabPanel /> 表示的是 children 中的每个 Tabs.Panel

联想到组件

<Tabs value={value}>
  <Tabs.Panel title="标签1">内容 1</Tabs.Panel>
  <Tabs.Panel title="标签2">内容 2</Tabs.Panel>
  <Tabs.Panel title="标签3">内容 3</Tabs.Panel>
</Tabs>

在这个案例中,React.Children 表示 children,即三个 Tabs.Panel,并且将其遍历

(item, index) => <TabPanel /> ,就一一对应每个 TabPanel

所以 children 中的内容必须是 Tans.Panel 组件,这样才不会出差错

同理,我们可以把标题抽离出来,成一”木偶组件”,React.Children.map 时传入所需要的数据

这里需要注意的是,Tabs.Panel 组件承担了 Tabs.Title 传值的作用,其 title、img 等都是传给 Tabs.Title 的参数。

做组件需要注意什么

  1. 它的结构

    • 我对 标签页组件是拆分为 TabsTabs.PanelTabs.Title。其中 Tabs.Title 没有暴露出来,因为不需要
  2. 它有什么属性

    • swipeable:支持滑动
      • 如果需要滑动,那么就需要用到 Swiper 滑块组件
      • 当它滑动时,标题中的 line 也要跟着滚动到相应的位置
    • sticky:粘性布局
      • 同样,粘性布局在很多场景下都有用到,是否需要抽离成组件
    • swipeThreshold:滚动阈值
      • 当 children 中的数量(React.Children.Count)大于阈值时支持横向滚动
  3. 它的下标跟随怎么做

    1. 首先要先获取每个 Tabs.Title 的 ref,即获取它的 dom

      1. Tabs.Title 时转发(forwardRef) 此组件
      2. 使用 <Title ref={(el) => (tabsTitleRef.current[index] = el)} ... /> 获取每个 Tabs.Title 的 dom
    2. 补间动画

      1. 拿到点击后的值的 dom 的 坐标,transform: translateX(XX)
    3. 当达到滚动阈值后,tabsNavRefTabs.Title 的父容器) 移动,移动是有公式的

      • function scrollLeftTo() {
          cancelRaf(rafId);
          let count = 0;
          const from = scroller.scrollLeft;
          const frames =
            duration === 0 ? 1 : Math.round((duration * 1000) / 16);
        
          function animate() {
            scroller.scrollLeft += (to - from) / frames;
        
            if (++count < frames) {
              rafId = raf(animate);
            }
          }
        
          animate();
        }
        

其余的没什么难度了

总结

Tabs 标签页组件的核心在于什么?标签的标题和内容,点击标签时,内容切换到该页面以及当它可以滑动时,变化的是当前的下标。我们需要以它为切入点,当它变化时,下划线滚动。而点击 Title 或者滑动 Content 都会触发 setCurrentIndex(index),用 hooks 写完美解决”关注点问题“

芝麻开门,显示全文!

回调函数到promise再到理解async/await

推特上有人发了个”在 7 秒内理解 async/await“ 的视频,地址看这里

拆分成就是写法的变化:回调函数 -> promise -> async/await

js 天生支持异步,如果你的数据依赖于异步请求,那么需要在它的回调中获取,一旦写的多了,就形成了回调地狱,如下图所示

回调函数模式

后来,ES6 出了 promise,promise 的意思是承诺,情景如下:

未婚妻:你一定要回来!

出去打战的士兵:I promise

这样写法上就有个先后顺序,不用再嵌套,而是串联(但换行之后看起来也很清晰)

promise模式

promise 的问题在于,它的语境还是异步,当 getDate 拿到数据后做事情(then)

人的惯性思维是同步,即写就写了,开心就开心了,怒就怒了,没有说等五秒后再笑

所以 promise 是虽好,但可以再换个写法——async/await

async/await模式

async/awiat 是绑定在一起的,缺一不可

const a = await getData() 其中的 a 就是请求数据拿到的结果,从理解上更符合人的思维

芝麻开门,显示全文!

闲人闲谈之执行力

最近一年来,开始将博客公开,为的是在在简历上给人一个能持续输出的印象。但本身却不爱在网上更多公开自己的信息,以至于当同事知道自己的 Github 后

主要是执行力不行。春夏天还行,但今年记录了一下,十月份过后(也许是十一综合征),状态就不行了

想挽回,即使执行了计划,还是不能很好的完成它

怎么能做到很好的执行自己的计划

迷茫有三:

为什么哪些大牛能一直持续更新?

  • 勤奋。强迫自己做
  • 兴趣。因为热爱不在乎
  • 生活。不得已

为什么我做不到?

  • 自身不够勤奋
  • 做了之后没有成就感,即使我写了文章,写了知识地图,但是成就感获得不多。但自己又不是那种特别爱成就感的人

设计属于自己的课程?

兴趣很廉价,专注力才可贵

芝麻开门,显示全文!

scss不能用除法?

之前在项目中就遇到过,scss 不是用除法的问题,当时项目忙,没有及时处理,但心里一直有一根刺,像张爱玲的红玫瑰一样闹的我心烦,现在有时间就想拔掉这根刺

错误定位

因为运行项目后,用到除法,提示错误为

scss警告不能除法

点进官方的解决方案

有两种,一种是引用 @use "sass:math";,使用 math.div(100%, 24) 这类写法,另一种是全局下载sass-migrator 对目标文件进行转换

网上查了一番, bootstrap 鸡贼,换了个思路,用乘法代替,具体可看代码。但也应该会遇到必须使用除法的情况,从 PR 上看,没有看到必须用除法的场景,可惜一番

算了,再去查一番,发现用指定绑定的 sass 可以,是个日本佬写的方案 。所以大概三种方法解决

先用第一种方法解决,确实,在开发环境上不报错了,但是在 build 时,报 Error: Invalid CSS after "...ion-delay: math"

报错信息

第二种、第三种都不好使

PS:这里要批评一下自己,看报错信息就知道使用到了 node-sass,node-sass 不支持这种写法,但是当时自己没认真看报错信息,直接去 Google 了

换种思路

我的项目是基于 umi 开发,用 scss 是因为安装了它的插件:@umijs/plugin-sass

umijs/plugin-sass文档

我的 @umijs/plugin-sass 已经升级到最新版本,也就是说使用到了 Dart Sass,难道 Dart Sass 的问题?

在胡乱找的时候,发现了这篇文章 ,解惑了

This is because you need to use sass instead of node-sass. Remove node-sass and use sass instead and this error should go away

翻译过来就是

这是因为您需要使用sass而不是node-sass. 删除node-sass并使用sass,此错误应该消失。

我的做法

直接升级整个项目,粗暴

npm update

接着删掉 package.json 中的 node-sass(当初的自己害了自己)

再接着删掉整个 node_modules,再重新下载

rm -rf node_modules
yarn

结果通了,这样解决了一个心头刺

@umijs/plugin-sass 源码解读

在找问题的时候看了下 @umijs/plugin-sass 的源码,贴出来看一下

import { IApi, utils } from "umi";

export default (api: IApi) => {
  api.describe({
    config: {
      schema(Joi) {
        return Joi.object({
          implementation: Joi.any(),
          sassOptions: Joi.object(),
          prependData: Joi.alternatives(Joi.string(), Joi.func()),
          sourceMap: Joi.boolean(),
          webpackImporter: Joi.boolean(),
        });
      },
    },
  });

  api.chainWebpack((memo, { createCSSRule }) => {
    createCSSRule({
      lang: "sass",
      test: /\.(sass|scss)(\?.*)?$/,
      loader: require.resolve("sass-loader"),
      options: utils.deepmerge(
        {
          implementation: require("sass"),
        },
        api.config.sass || {}
      ),
    });
    return memo;
  });
};

介于对 webpack 的不熟悉,下面的说法不带有参考性

  • api.describe 不懂
  • api.chainWebpack 这段大概是对 webpack 的规则的一些写入,即是用 sass 写法

后续学习前端工程化系列的时候再对其做补充

扩展阅读

Sass 是采用 Ruby 语言编写的一款 CSS 预处理语言

Sass 和 Scss 其实就是同一种东西,我们平时都称之为 Sass,两者不同之处主要有两点:

  1. 文件扩展名不同,Sass 是以“.sass”后缀为扩展名,而 Scss 是以“.scss”后缀为扩展名。
  2. 语法书写方式不同,Sass 是以严格的缩进式语法规则来书写,不带大括号({})和分号(;),而 Scss 的语法书写和我们的 CSS 语法书写方式非常类似。

芝麻开门,显示全文!

css定位小技巧

最近在忙着做组件,发现之前的组件做的不好看,其关键在于我们没有 UI 标准,所有的标准由产品定,设计就出一个看上去大差不差的效果图(尺寸没有按标准来做,多少会差个几像素)。然后开发按照原型标准来做

这倒是的我们前端出的产品处于没有标准但看上去是一回事的状况

这不,在重构过程中,我发现之前的组件的不合理之处

在做 Tag 标签组件时,发现加 border 的标签比其他的都长了

image-20211111142921445

查看 CSS,发现是因为 border 的长度导致,因为这里有三个标签,加起来就有 6 个像素,所以看起来就很明显

使用 box-sizing: border-box 更改盒模型,回顾一下,盒模型有哪几种,

标准盒模型

  • width= content

IE 盒模型(怪异盒子)

  • width=content+border+padding

但问题是我所有的盒模型都已经格式化为 border-box。所以这个方法没有用

参考了一下同行的做法,这里给有赞一个赞

他们的做法是对主 UI 做相对定位,再在 before 中添加绝对定位,即

.Tag {
  position: relative;
  &::before {
    position: absolute;
    top: 0;
    right: 0;
    bottom: 0;
    left: 0;
    border: 1px solid;
    border-color: inherit;
    border-radius: inherit;
    content: "";
    pointer-events: none;
  }
}

这样就做到了在 主容器中(Tag 组件)实现边框的内嵌

芝麻开门,显示全文!

我的历史梳理

前言

在微博写周末闲谈,写着写着就超过了一千字,想着干脆写完整了,放在博客上,记录一下 27 岁时我的历史观

正文

最近这两周来,一直在看温铁军教授的视频,很受感触,从三农问题说到国家战略;从国际政治说到马列主义,再到中国特色社会主义;从东西方文明接触的始点说到现在的贸易战,再到中国的应对。总之,我的历史观有被重塑。强烈推荐朋友们去看一看

这里我抛砖引玉把我的所知所记写下来,东西在脑子里,想到哪写什么

秦始皇统一中国,二世被灭。起源于不在于秦的暴政,而是他“车同轨,书同文,统一度量尺”等一些列措施得罪了各种利益阶级。像车同轨,得罪的就是贵族,为什么呢?各个国家的车轨尺寸不同,你一统一,尺寸不同意味着身份不同;同理书同文得罪的是文人;统一度量尺得罪的是商人(商人可在各个国家套利)。秦统一为了管理整个天下,必须要中央集权,搞郡县制,不再搞分封,毕竟周朝的分封已经证明失败,但要把不统一改为统一,必然招到反噬

像现在 200 多个国家和地区,要是有强人想统一整个地球,即靠武力统一地球,实现统一语言、文字、标准,阻力肯定很大,也许根本推行不下去。秦那个时候就处于那样的状况,结果是屁股还没坐稳几年,就被灭了

到了汉朝,前几代皇帝休养生息,文景之治就在这时发生的,安稳了几年,国库里有了存款。汉武帝也是千古一帝,他想打匈奴了。再此之前,汉朝是要每年给匈奴钱,交不起就侵略,汉武帝觉得国库有钱了,不想给钱了,就硬气起来和他们干起来了

其实匈奴来犯也是有原因的,因为那个时候正处于小冰河时期,气候正好变冷,他们必须南下生存,也是迫不得已

汉武帝做到了每户人家没儿子的必须养马,有男丁的必须出一人,去打战时是一个人牵着两匹马上的,霍去病、卫青、李广就是那个时候的出来的英雄,因为他们打的是外来侵略者,所以在历史上的名气大,而秦国时期大将白起、蒙恬知道的人就少了(对老百姓而言)

匈奴被汉武帝打败后,还是要生活啊,怎么办,往西走,翻过喀尔巴阡山脉,进入东欧,跟欧洲的原始部落冲突,最终进入中欧、北欧一代,把这一代的日耳曼蛮族南压,罗马人打不过,允许日耳曼南下,进入意大利,最终是日耳曼蛮族灭了西罗马

简单来说,东方把匈奴(游牧民族)打到大漠以北,匈奴进入欧洲,引起欧洲动乱,使得西罗马灭亡(外因)。这次天气变化引起的打仗、让东西方文明都倒退了。东方倒退了 400 年,期间经历了东汉、三国、魏晋、十六国、南北朝,直到隋唐又一大一统;西方进入中世纪,直接倒退 1000 年,抗打击能力属实不行。

而在唐朝时期,听说又是因为天气原因。不过面对的是突厥。突厥分东西突厥。唐朝李世民照样打,打败了突厥后,吸纳了一部分,即新疆地区同胞,而另一部分突厥人往西跑,经过突厥走廊,并南下,到了今天的土耳其(安纳托利亚半岛),导致了东罗马(拜占庭)灭亡的一个外因

这就是两次文明冲突的历史梳理

温老讲课讲的极好,我一个对历史没多大兴趣的人都听得津津有趣,推荐大家可以去B站看一下他的课

芝麻开门,显示全文!

搞轮子:皮肤概念的几种方案

而且我还希望能有灰度模式和暗黑模式,皮肤概念孕育而生

大厂的做法

vant

ant-design

zarm

考察下来,zarm 最符合我的预期

按 zarm 的来

简单来说,使用 context 来做主题色,因为 context 能传递到任意组件,接下来就看怎么写

皮肤解决方案

https://segmentfault.com/a/1190000041195585?_ea=195391016

CSS 变量

:root {
  --bg-color: brown; // 定义颜色变量
}
.btn {
  // 直接使用颜色预定义的颜色变量
  background-color: var(--bg-color);
}

https://css-tricks.com/switch-font-color-for-different-backgrounds-with-css/

https://zhuanlan.zhihu.com/p/494460951

https://mp.weixin.qq.com/s/6bmqki5IPDlD4H7a7C1HXw

张鑫旭的换肤方案

https://www.bilibili.com/video/BV1kU4y1X7a8?vd_source=55c655c3b4aed7bb7a250da7eea13eb8

https://juejin.cn/post/7117911005841063944

芝麻开门,显示全文!

项目实战:服务器中的nginx和docker起的nginx冲突怎么办

今天,后端过来说有个需求需要前端帮忙,我立即摆谱:“哼,什么问题?”

他连忙跑到我的座位前,窜着手机跟我说:“我发你一个链接,能不能把这些代码放到 hosts 文件里。”

我说:“这是什么?为什么要放。”

经过他的解释,大致是

前端点击一个按钮,调用 API ,后端返回一个 url,url 是第三方链接(后端对接第三方公司返回的链接),因为第三方链接的测试环境需要内网环境才能测试,所以要测试这个产品需要配置 hosts 文件

我的理解是,这个只需要在本机上添加 hosts 文件,让测试添加即可,管我前端什么事!

后来经过拉锯战,他找来技术负责人,负责人亲自讲解,希望我把这些代码放到前端服务器的 hosts 文件中

没办法,说了不听,听了不悦,要我做。官大一级压死人。这里也很有意思,技术人员中,如果你技术真的过硬,听你的,确实没问题,到后面下级还能学到东西。但是如果上级技术一般,这个知识点不是很懂,那么只能试,试就需要成本,这次就是一次成本代价

我在服务器上的 hosts 中添加代码后,网站就访问不了了,nginx 502

我感觉删掉添加的代码,再重启 DNS 服务,结果还是不行

继续搞,搞半天才想起来我的站点部署在 docker 中,启动服务器的 nginx 没用

搞 docker

常用命令

  • docker ps 查看运行的容器
  • docker exec -it containerID /bin/bash 进入指定容器的 docker 服务

先删掉原本的 docker,在手动启动 docker 命令(同事写好的 bash 文件)

直接报错,反向代理不能用字母

host not found in upstream "XXX.com" in /etc/nginx/conf.d/default.conf:18
nginx: [emerg] host not found in upstream "XXX.com" in /etc/nginx/conf.d/default.conf:18

暂时找不到原因,先改,改成 ip 后,启动成功,使用

docker exec -it containerID /bin/bash 进入容器中,查看 docker 中的 nginx 和静态文件是否有问题,发现都是最新的,理论上是没问题的

回到服务中,再查看端口占用

ps -ef | grep nginx

有好几个 nginx 的服务是启动的

所以猜想是不是 hosts 文件添加之后,docker 自动被弄坏了,然后我再启动了 nginx,nginx 和 docker 启动的 nginx 冲突,所以即使把 hosts 文件恢复了,因为 nginx 一直启动着,所以 docker 启动的容器一直不能访问

解决方案

简单来说,把服务器中的 nginx 关掉,再重启 docker 容器即可

一、查找 nginx 所占的端口

ps -ef | grep nginx

二、杀掉所有的与 nginx 相关的端口

kill -9 12782

三、重启 docker 相关容器

docker restart f4d

总结

到最后解决方案很简单,但主要是排查能力和对命令的熟悉

这次排查唤起了我对很多 nginx 和 docker 的知识点,明年要对这两块做重新的知识梳理

芝麻开门,显示全文!

左边固定宽,右边自适应的6种方法

这是一道面试题,你有多少种办法呢?

这里我们假设左边名为 left,宽度为 200 px,右边名为 right。即默认

.left {
  width: 200px;
}

我的理解分四大类

  • flex 布局
    • 需设置父元素高度
  • grid 布局
    • 需设置父元素高度
  • 绝对定位
    • 双子元素 absolute
      • 不需要设置父元素高度
      • 子元素都设置高度,右边子元素 left:200px + width: 100%
    • 左元素 absolute + 右元素 margin-left
      • 不需要设置父元素高度
      • 子元素都设置高度,右边子元素 margin-left: 200px + width: 100%
  • float 浮动
    • 左元素左浮动,右元素不动
      • 无需父元素
      • 左元素需设置宽高和浮动,右元素设置高度即可
    • 左元素左浮动,右元素右浮动
      • 无需父元素
      • 左元素设置宽高和左浮动,右元素设置右浮动以及高和宽(width: calc(100% - 200px)

flex 布局

需要一个父元素做 flex 布局,且需要给它一个高度(撑开容器)

.father {
  display: flex;
  height: 200px;
}
.right {
  flex: 1;
}

grid 布局

高级的布局方式,子元素不需要设置宽度,单单设置父元素属性即可。

.grid {
  display: grid;
  grid-template-columns: 200px 100%;
  height: 200px;
}

双子元素 + absolute

需要给子元素设置宽高,不然撑不起来。右元素设置left: 200px

.father {
  position: relative;
  height: 200px;
}
.left {
  position: absolute;
  height: 200px;
}
.right {
  position: absolute;
  left: 200px;
  height: 200px;
  width: 100%;
}

左元素 absolute + 右元素 margin-left

.father {
  position: relative;
  height: 200px;
}
.left {
  position: absolute;
  width: 200px;
  height: 200px;
}
.right {
  width: 100%;
  height: 200px;
  margin-left: 200px;
}

无父元素 + 左元素左浮动,右元素不动

前两种都需要有个父元素,但浮动不需要

左边浮动,下一个元素独占位置,并排一行

同样,需要设置高度,子元素才能撑开

.left {
  float: left;
  height: 200px;
}
.right {
  height: 200px;
}

无父元素 + 左边左浮动,右边有浮动

浮动不需要父元素,浮动就区别于正常文档流

我的理解是正常文档流是二维层面,而浮动相当于成了三维,区别于 Z 轴

右边元素有浮动不够,还需要设置宽度

.left {
  float: left;
  height: 200px;
}

.right {
  float: right;
  height: 200px;
  width: calc(100% - 200px);
}

只要是 float 实现此功能的,都不需要父元素,以及自身都需要设置高度

总结

简单来说,实现布局最好的方式是 flex,简单兼容现代浏览器和机型。当然,我是因为还没有学 grid(但 grid 要记得参数比较多)。绝对定位和浮动各有优缺点

各大方法优缺点需要什么
flex布局简单需要父元素、高度。子项 flex:1
grid布局最简单,但兼容性更现代只需要父元素设置属性就好
绝对定位兼容性更高需要父元素做相对定位、高度
浮动兼容性更高不需要父元素,子项都需要宽高

float 区别于其他三种,不需要父元素做容器

grid 区别于其他三种,不需要设置子元素(左元素的)宽

绝对定位区别于其他三种,它的方法不仅要父元素有高,其子元素也要有高

flex 最简单

附上线上 demo

芝麻开门,显示全文!

水平垂直居中的17种方法

面试的时候,绝不能只说一种,绝不能说一种解决方案,绝不能停下你吞吞吐吐的嘴

CSS 方面问的最多的问题之一,我想分三种情况,水平居中、垂直居中和水平垂直居中来分析

单单就水平垂直居中而言,大概有以下几种方案:

居中元素不定宽高

  • absolute + transform
  • flex 属性居中
  • flex + 子项 margin auto
  • grid 属性居中
  • grid + 子项 margin auto
  • grid + 子项属性居中
  • -webkit-box 属性居中
  • table-cell + text-align
  • line-height + text-align
  • writing-mode
  • table

仅居中元素定宽高适用:

  • 须知宽高 + absolute + 负 margin
  • 须知宽高 + absolute + calc
  • 须知宽高 + absolute + margin auto

局限性较大的全局居中

  • 须知宽高 + fixed + transform
  • 须知宽高 + fixed + 负 margin
  • 须知宽高 + fixed + calc
  • 须知宽高 + fixed + margin auto

水平居中

text-align: center

text-align: center;

需设置 display: inline-block 行内块元素

绝对定位 + transform 位移

position: absolute;
left: 50%;
transform: translateX(-50%);

脱离文档流

宽度+ margin: 0 auto

width: 100px;
margin: 0 auto;

这里说明下,width:100px 必须是具体的数字,且这个居中是外层居中,宽度中的内容没有居中

垂直居中

绝对定位 + transform 位移

position: absolute;
top: 50%;
transform: translateY(-50%);

与水平方向的居中一样,都是脱离文档流的做法

table-cell + vertical-align

display: table-cell;
vertical-align: middle;

display: table-cell ,让其标签元素以表格单元格的形式呈现,类似于 td 标签,

vertical-align: middle,用来指定行内元素(inline)或表格单元格(table-cell)元素的垂直居中

水平垂直居中

绝对居中 + transform 位移

<div class="father">
  <div class="son">123123</div>
</div>
.father {
  position: relative;
}
.son {
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
}

flex 属性居中

<div class="father">
  <div class="son">123123</div>
</div>
.father {
  display: flex;
  justify-content: center;
  align-items: center;
}

flex + margin auto

<div class="father">
  <div class="son">123123</div>
</div>
.father {
  display: flex;
}
.son {
  margin: auto;
}

grid 属性居中

<div class="father">123123</div>
// 或者
<div class="father">
  <div class="son">123123</div>
</div>
.father {
  display: grid;
  justify-content: center;
  align-items: center;
}

grid 子项属性居中

<div class="father">
  <div class="son">123123</div>
</div>
.father {
  display: grid;
}
.son {
  align-self: center;
  justify-self: center;
}

grid + margin auto

<div class="father">
  <div class="son">123123</div>
</div>
.father {
  display: grid;
}
.son {
  margin: auto;
}

grid 和 flex 很像,是 flex 的升级版,所以 grid 能做的事情更多

以上绝对定位、flex、grid 关于水平垂直居中的做法,剩下再说居中比较老的布局方法

-webkit-box 属性居中

这是一个已经过时的布局,可以看看这篇文章 CSS3 display: flex 和 display: box 有什么区别?

网友一丝说:

flex 是 2012 年的语法,是以后的标准

box 是 2009 年的语法,已经过时,需要加上对应前缀

<div class="father">
  <div class="son">123123</div>
</div>
.father {
  display: -webkit-box;
  -webkit-box-pack: center;
  -webkit-box-align: center;
}

table-cell + text-align

<div class="father">
  <div class="son">123123</div>
</div>
.father {
  display: table-cell;
  vertical-align: middle;
  text-align: center;
}
.son {
  display: inline-block;
}

line-height + text-align

<div class="father">
  <div class="son">123123</div>
</div>
.father {
  height: 200px;
  line-height: 200px;
  text-align: center;
}

line-heightheight ,行高和高度一样高,自然就垂直方向居中了

writing-mode

<div class="father">
  <div class="“son”">
    <div class="“sonson”">123123</div>
  </div>
</div>
.father {
  writing-mode: tb-lr;
  writing-mode: vertical-lr;
  text-align: center;
}

.father .son {
  writing-mode: lr-tb;
  writing-mode: horizontal-tb;
  text-align: center;
  width: 100%;
  display: inline-block;
}
.father9 .son .sonson {
  display: inline-block;
  text-align: initial;
}

这个很冷闷,有人介绍过这种情况

table

<table>
  <tbody>
    <tr>
      <td class="father">
        <div class="son">123123</div>
      </td>
    </tr>
  </tbody>
</table>
.father {
  text-align: center;
}

table 标签自己将它垂直居中了,text-align:center 后就是水平居中了

可以看 demo

元素有宽高的情况,又多了三种方案

须知宽高 + 绝对居中 + margin 负边距

<div class="father">
    <div class="son">
        123123
    </div>
</div>
.father {
  position: relative;
  height: 200px;
}
.son {
  width: 100px;
  height: 100px;
  position: absolute;
  top: 50%;
  left: 50%;
  margin: -50px 0 0 -50px;
}

父元素必须要有个高度,这样才能撑开容器。子元素必须要有个宽高,才能计算出 margin 值

须知宽高 + 绝对定位 + calc

<div class="father">
  <div class="son">123123</div>
</div>
.father {
  position: relative;
  height: 200px;
}

.son {
  width: 100px;
  height: 100px;
  position: absolute;
  top: calc(50% - 50px);
  left: calc(50% - 50px);
}

与 margin 负边距一个道理,父元素需要设置一个高度。子元素必须要有高度,不用 margin,而用 CSS3 中的 calc,计算出要居中位移,兼容性需要支持 CSS3 属性

须知宽高 + 绝对居中 + margin: auto

<div class="father">
  <div class="son">123123</div>
</div>
.father {
  position: relative;
  height: 300px;
}

.son {
  width: 100px;
  height: 100px;
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  margin: auto;
}

同以上两种情况。

这三种需要定位方式来实现水平垂直居中的方法,需要设置父元素的高度(一定要有,撑开画面),子元素需要设置宽高,前两种方法是为了算出它在父元素中的相对位置,后一种方法是为了说明子元素是个容器(如果不设置宽高,就是无)

其他方法

其实,水平垂直居中方面,如果面试官硬要问还有吗?还真的有,用 fixed 定位。但这个方法有缺陷,虽然能实现水平垂直居中,但它是相对于视口(viewport),而非父元素

方法就是以上用 absolute 实现的改成 fixed 即可

  • 须知宽高 + fixed + transform
  • 须知宽高 + fixed + 负 margin
  • 须知宽高 + fixed + calc
  • 须知宽高 + fixed + margin auto

这四种方法,都需要设置子元素的宽高

这里贴一下代码

<div class="father">
  <div class="son">123123</div>
</div>
/* transform */
.son {
  width: 100px;
  height: 100px;
  position: fixed;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  background: blue;
}

/* 负 margin */
.son {
  width: 100px;
  height: 100px;
  position: fixed;
  top: 50%;
  left: 50%;
  margin-left: -50px;
  margin-top: -50px;
  background: blue;
}

/* calc */
.son {
  width: 100px;
  height: 100px;
  position: fixed;
  top: calc(50% - 50px);
  left: calc(50% - 50px);
  background: blue;
}

/* margin: auto */
.son {
  width: 100px;
  height: 100px;
  position: fixed;
  top: 0;
  left: 0;
  bottom: 0;
  right: 0;
  margin: auto;
  background: blue;
}

总结

随着微软宣布放弃 IE11,现实项目中完全可以使用 flex 布局,grid 部分还不适配,但是以后肯定会取代 flex。

虽然写了很多,但是自己工作中也不会使用 table 、writing-mode、-webkit-box 等过时的布局方式,写这篇文章,纯粹是为了面试时被问到这种问题。

收获是 absolute 的居中要父子同心(父元素设置高度,子元素设置宽高),fixed 的居中只需要设置子元素的宽高。

线上 demo 查看

参考资料

芝麻开门,显示全文!