UU笔记

这位博主叫 UU,以前微博名叫:拉瓦铀,后来因为微博误封,他索性换了个号,现在的号叫 笛卡吾 ,他有个自己的网站叫锡安,我在春节放假期间,付费读了他的文章,因为他写的东西太长,有些内容又晦涩,我只记录下一些个人觉得有用的话

  • 地理决定生物,生物影响历史,历史衍生文化,文化催生科学,科学发现物理,物理解释地理
  • 绝大多数人根本想不明白,不理财 == 被剥削
  • 虚拟币不仅是黄金还同时是古董
  • 做人不要自我设限
  • 在知识的海洋里,不要做一个农民,要做一个游牧者
  • 人都有惰性,都喜欢能「一招鲜,吃遍天」,都盼着一劳永逸地在社会里找到一个舒服的位置躺着生活,但这注定不可能实现
  • 稳定,就是中国政治的金线
  • 不要让生活品质下降,否则人的心态会受到很大的影响
  • 很多没有历史常识的人,羡慕民国,甚至羡慕魏晋,就是这个原因——下面的人不算人,看不见;上面的人很稳定,所以有足够的钱和闲花样作,又是大师,又是七贤
  • 300 多万同志在扶贫一线,1800 名英雄牺牲。6 亿中国人,平均月收入不到 1000 元。时刻提醒自己这两组数据,就不会脱离群众,就不会站在人民的对立面
  • 人一定要趁年轻多运动,做一些负重,不要只顾着心肺。人老了,肌肉会持续萎缩,力量越来越小,再想增肌基本上不可能了,就是维持着。肌肉量越小,越容易疲惫,精神越差,越不可能增肌,恶性循环。很多老人,一早就开始慢性疼痛,等死,就是因为掉进了这个循环里。有什么办法吗?其实没有……医学再发达,只能有病治病,没病止痛,不可能凭空生肌。到了那个时候,就晚了。这是真正意义上的「等你老了就知道了」。因为雌性的雄激素水平远低于雄性,而肌肉量主要与雄激素有关,所以,年轻女性更要注意提前装备一些骨骼肌,等老了维持体能。女性的激素水平波动大,如果涉及生育哺乳的话,波动就更大,更年期又是一道坎。千万不要中年发福,变成梨形,不然过了 40 再想瘦下来,难上加难。而且人越老,嘴越馋,意志力下降的同时,生活兴趣收缩。会活得越来越像动物,这是大规律,无法逆转。假如只剩下吃喝了,那养老生活就跟堆肥是一样的。趁年轻,多运动。懂的人懂。

芝麻开门,显示全文!

圣杯布局和双飞翼布局

前言

虽然这类面试题已经很久没看到了,但作为2022年春节假期的第一天,轻松为主,拿来试试刀

作用

首先要解释一下:无论是圣杯布局还是双飞翼布局,都是为了实现一个效果:左右固定宽度,中间部分自适应。两者的区别在于,圣杯布局给中间的 div 设置 padding-left 和 padding-right;而双飞翼布局则在中间的 div 内部创建子 div 放置内容,并在该 div 里用 margin-left 和 margin-right 留出左右宽度

圣杯布局

HTML 结构如此:

<body>
  <header>组成头部</header>
  <section class="container">
    <div class="middle">中间部分自适应</div>
    <div class="left">左边栏固定宽度</div>
    <div class="right">右边栏不顾宽度</div>
  </section>
  <footer>组成尾部</footer>
</body>

CSS 样式如此:

body {
  min-width: 700px;
}

header,
footer {
  background: grey;
  border: 1px solid #333;
  text-align: center;
}

.left,
.middle,
.right {
  position: relative;
  float: left;
  min-height: 130px;
}

.container {
  padding: 0 220px 0 200px;
  overflow: hidden;
}

.middle {
  width: 100%;
  background: red;
}

.left {
  margin-left: -100%;
  left: -200px;
  width: 200px;
  background: green;
}

.right {
  margin-left: -220px;
  right: -220px;
  width: 220px;
  background: blue;
}

footer {
  clear: both;
}

效果如此:

圣杯布局

代码说明:

  1. 首先在容器 container 中给予 padding:0 220px 0 200px ,这一步是为了给两边固定宽预留位置
  2. 中间(middle)元素设置 width: 100%,它自然就自适应了
  3. 设置左边(left)元素 position: relative, left: -200px。这样做是让它做到左边的固定位置,做到此时,效果如下
    • 圣杯布局
    • 接着用margin-left: -100%,让其回到与中间部分一样高的位置,这就是圣杯布局的关键
    • margin-left:-100% 表示向左移动整个屏幕的距离
    • 因为三个元素都加了浮动,所以配合 margin-left: -100% 左边容器就能与中间部分同高
  4. 同理,设置右边(right)元素position: relative, right:-220px, margin-left: -220px
    • 注意,这里的 margin-left 是 220px。为什么这个是220px,和它自身宽度一致
    • 注意 ,自身 margin-left 为负时,就往左移动,等它等于自身高度时就“升格”到上一层,当它等于 -100% 时,这个 100% 表示的是中间部分的宽度,所以就固定在左边了

双飞翼布局

有人说“双飞翼布局是玉伯大大提出来的,始于淘宝UED”,其效果和圣杯布局一样,只是它把三栏布局比作一只鸟,中间内容部分分为三部分,左翅膀、中间、右翅膀。其技术关键在于它还有一层 div

HTML 结构如此:

<body>
  <header>组成头部</header>
  <section class="container">
    <div class="main">
      <div class="middle">中间部分自适</div>
      <div class="left">左边栏固定宽度</div>
      <div class="right">右边栏固定宽度</div>
    </div>
  </section>
  <footer>组成尾部</footer>
</body>

CSS 结构如此:

body {
  min-width: 700px;
}

header,
footer {
  background: grey;
  border: 1px solid #333;
  text-align: center;
}

.left,
.right,
.main {
  float: left;
  min-height: 130px;
}

.main {
  width: 100%;
  background: red;
}

.main-inner {
  margin: 0 220px 0 200px;
  min-height: 130px;
}

.left {
  margin-left: -100%;
  width: 200px;
  background: green;
}

.right {
  margin-left: -220px;
  width: 220px;
  background: blue;
}

footer {
  clear: both;
}

效果如圣杯布局一致,不重复展示,它代码的关键在于先构建中间部分展示出,再通过 margin-left 完成浮动流

思考:为什么会考三栏布局?

以前的布局难点就是三栏布局,而且三栏布局还能引出 BFC,BFC 的作用之一就是自适应布局。而现在 flex: 1就能解决自适应布局的问题,所以这类考题已经不多见了

总结

以前一直担心考这类破问题,因为完全没准备过。除了一次考左边固定宽,右边自适应外,就没考过 CSS 布局方面的问题,大概是因为已经过时了

三栏布局两种解决方法

  • 圣杯布局
    • 浮动 + margin-left + 自身相对定位
  • 双飞翼布局
    • 浮动 + margin-left + 中间部分再包裹一层

相同点:浮动 、margin-left

margin-left: -100% :左边上升

margin-left: -220px:右边上升

线上demo:

参考资料

芝麻开门,显示全文!

面试题:箭头函数和普通函数的区别

一个东西你知道知道的多,就能写出的多

去年面试的时候,五位面试官有三位问到了这个问题,可见这是一个面试常题,我都忘记自己是怎么回答的,要我现在说:箭头函数没有 this 绑定,它的 this 指向父作用域

果然,记忆记不牢是有原因的,因为没有写文章,没有理解真正理解它

真正的答案是什么?

阮一峰版

  • 箭头函数没有自己的 this 对象,函数体内的 this 是定义时所在的对象而不是使用时所在的对象
  • 不可以当作构造函数,也就是说,不可以对箭头函数使用 new 命令,否则会抛出一个错误
  • 不可以使用 arguments 对象,该对象在函数体内不存在。如果要用,可以用 rest 参数代替
  • 不可以使用 yield 命令,因此箭头函数不能用作 Generator 函数
  • 返回对象时必须在对象外面加上括号

尼古拉斯版

  • 没有 this、super、arguments 和 new.target 绑定。this、super、arguments 和 new.target 的值由最近的不包含箭头函数的作用域决定
  • 不能被 new 调用,箭头函数内部没有 [[Construct]] 方法,因此不能当作构造函数使用,使用 new 调用箭头函数会抛出错误
  • 没有 prototype,既然你不能使用 new 调用箭头函数,那么 prototype 就没有存在的理由。箭头函数没有 prototype 属性
  • 不能更改 this, this 的值在函数内部不能被修改。在函数的整个生命周期内 this 的值是永恒不变的
  • 没有 arguments 对象,既然箭头函数没有 arguments 绑定,你必须依赖于命名或者剩余参数来访问该函数的参数
  • 不允许重复的命名参数

尼古拉斯是写《深入理解 ES6》的作者,阮一峰就不解释了

结合起来,就是说箭头函数和普通函数的区别在于:

  • 它不能被当作构造函数,因为它不能被new,不能被 new 的原因在于箭头函数内部没有 [[Construct]] 方法。又因为它不能被 new,所以也就没有 prototype
  • 它没有自己的 this,它的 this 由定义时所在的对象决定而不是使用时所在的对象
  • 它也没有 arguments 对象
  • 不可以使用 yield 命令,不能用作生成器函数

我们依次说说这四点

new 从何来

先复习一下 new 调用构造函数会执行什么

  1. 在内存中创建一个新对象
  2. 这个新对象内部的 [[prototype]] 特性被赋值为构造函数的 prototype 属性
  3. 构造函数内部的 this 被赋值为这个新对象(this指向新对象)
  4. 执行构造函数内部的代码(给新对象添加属性)
  5. 如果构造函数返回非空对象,则返回该对象;否则,返回刚创建的新对象

我们可以手写一个 new

function new2(Constructor, ...args) {
  var obj = Object.create(null);
  obj.__proto__ = Constructor.prototype;
  var result = Constructor.apply(obj, ...args);
  return typeof result === "object" ? result : obj;
}

复习完 new,回过头看为什么不能调用 new

JavaScript 函数内部有两个内部方法:[[Call]] 和 [[Construct]]

  • 直接调用时执行[[Call]] 方法,直接执行函数体
  • new 调用时执行 [[Construct]] 方法,创建实例对象

箭头函数设计之初是为了设计一种更简短的函数,没有 [[Construct]] 方法。具体99.9%的人都不知道的箭头函数不能当做构造函数的秘密 摘出了很多英文材料佐证这个事实

我们可以这样说,因为它没有[[Construct]] 内部方法,所以它不能被 new。而因为它不能被 new,所以它也没有 prototype

prototype 的理解可以看这篇: 原型

this 谁人调用你

JavaScript 中的 this 是词法作用域,与你在哪里定义无关,而与你在哪里调用有关,所以会有各种 this “妖”的问题,改变 this 有 4 种方法

  • 作为对象方法调用
  • 作为函数调用
  • 作为构造函数调用
  • 使用 apply 或 call 调用

但是箭头函数没有自己的 this 对象,内部的 this 就是定义时上层作用域中的this。也就是说,箭头函数内部的 this 指向是固定的

arguments 老一辈的类数组

arguments 是一个对应于传递给函数的参数的类数组对象。arguments 对象标识所有(非箭头)函数可用的局部变量,可以说只要是(非箭头)函数就自带 arguments,它表示所有传递给函数的参数

什么是类数组对象

所谓类数组对象,就是指可以通过索引属性访问元素并且拥有 length 属性的对象

var arrLike = {
	0: 'name',
	1: 'age',
	length: 2
}

箭头函数没有

yield 是什么

说 yield 之前,先了解下生成器

生成器是 ES6 新增的一个极为灵活的结构,拥有在一个函数块内暂停和恢复代码执行的能力。

生成器的形式是一个函数,函数名称前面加一个星号(*)表示它是一个生成器。只要是可以定义(非箭头)函数的地方,就可以定义生成器

// 生成器函数声明
function* generatorFn() {}

// 生成器函数表达式
let generatorFn = function* () {};

// 作为对象字面量方法的生成器函数
let foo = {
  *generatorFn() {},
};

// 作为类实例方法的生成器函数
class Foo {
  *generatorFn() {}
}

// 作为类静态方法的生成器函数
class Bar {
  static *generatorFn() {}
}

标识生成器函数的星号不受两侧空格的影响

而 yield 关键字是可以让生成器停止和开始执行,也是生成器最有用的地方。生成器函数在遇到 yield 关键字之前会正常执行。遇到这个关键字后,执行会停止,函数作用域的状态会被保留。停止执行的生成器函数只能通过在生成器对象上调用 next() 方法来恢复执行

// umi 项目中请求接口时的例子
*fetchData({ payload }, { call, put }) {
    const resData = yield call(fetchApi, payload);
    if (resData.code === 'OK') {
        yield put({
            type: 'save',
            payload: {
                data: resData,
            },
        });
    } else {
        Toast.show(resData.resultMsg);
    }
},

因为箭头函数不能用来定义生成器函数才不能使用 yield 关键字

模拟面试

面试官:对 ES6 了解吗

面试者:嗯呢,项目中一直有用

面试官:你说说你平时都用哪些 ES6 的新特性

面试者:例如箭头函数、let、const、模板字符串、扩展运算符、Promise…

面试官:嗯嗯,箭头函数和普通函数有什么区别

面试者:箭头函数不能被 new、没有 arguments、它的 this 在那里定义相关、它不能用 yield 命令,返回对象时必须在对象外面加上括号

面试官:箭头函数为什么不能被 new

面试者:因为箭头函数没有 [[Construct]] 方法,在 new 时,JavaScript 内部会调用 [[Construct]] 方法,因为箭头函数没有,所以 new 时会报错。当然,因为不能被 new ,所以箭头函数也没有 prototype

面试官:你刚刚说到没有 arguments,简单介绍下它

面试者:它是所有参数的合集,每个(非箭头)函数自带 arguments,其结构是类数组对象

面试官:什么是类数组对象

面试者:可以通过索引访问元素且拥有 length 属性的对象…

面试官:我问问其他的

参考资料

芝麻开门,显示全文!

图片懒加载的三种解决方法

写作提高思考

前言

我想写一个系列,关于图片懒加载、React 渲染十万条数据、无限下拉方案文章,因为其三者有共性,都有使用了 与 DOM 相关的 offsetTop、innerHeight、getBoundingClientRect、IntersectionObserver 等,这些知识点如果单独放在一篇文章中,其价值点就是1,如果相互连接,价值点就是3。用好梅特卡夫定律,能让自己的效率提升不少

正文

学习前端,三板斧,HTML、CSS、JavaScript。JavaScript 基础由 DOM、BOM、ECMAScript 组成,其中 ECMAScript 为规范语言,现在说的 ES6(ES2016~ES2022) 指的就是它,隔段时间就会发布,目前一年发布一次,以年份来说,现在是 ECMAScript 2022。BOM是什么,BOM是浏览器对象模型(Browser Object Model)。它有六大对象

  • document: DOM(对 BOM 包含了DOM,但是 DOM 重要,其地位和 BOM 一样)
  • event:事件对象
  • history:浏览器的历史记录
  • location:窗口的 url 地址栏信息
  • screen:显示设备的信息
  • navigator:浏览器的配置信息

DOM我们也很了解,文本对象模型,指操作HTML(超级文本标识语言)的API。DOM 会将文档解析为一个由节点和对象(包含属性和方法的对象)组件的结构集合

以前开发页面时,我们在 script 标签中,先获取节点(DOM Api),再操作 DOM ,所以以前是 《JavaScript 面向对象编程》,《JavaScript dom编程艺术》,但操作 DOM 的 API 太长,不易书写,JQuery 集大成,简化Api,统一了操作写法。如果展开,会有很多可以延伸,而我铺垫了这么多,就是想引出 document

这次我们要讲的 offset、scroll、client 就是出自“ document 家”。先配两张图来看看这三个到底是什么

document

document2

client

client 指元素本身的可视内容。不包括 overflow 被折叠部分,不包括滚动条、border,包括 padding

有四属性:

  • clientHeight:对象可见的高度
  • clientWidth:对象可见宽度
  • clientTop:元素距离顶部的厚度,一般为0,因为滚动条不会出现在顶部
  • clientLeft:元素距离左侧的厚度,一般为0,因为滚动条不会出现在左侧

offset

offset 指偏移。包括这个元素在文档中占用的所有显示宽度,包括滚动条、padding、border,不包括 overflow 隐藏的部分

有五属性:

offsetHeight:该对象自身的绝对高度

  • offsetHeight: = border-width * 2 + padding-top + height + padding-bottom

offsetWidth:该对象自身的绝对宽度

  • offsetWidth = border-width * 2 + padding-left + width + padding-right

offsetParent:返回一个对象的引用,字面意思,相对父元素的偏移

  • 如果当前元素的父元素没有 CSS 定位(position为absolute/relative),offsetParent 为 body
  • 如果当前元素的父元素有 CSS 定位(position为absolute/relative),offsetParent 取父级中最近的元素

offsetTop:相对版面或 offsetParent 属性指定父坐标的顶部距离

  • offsetTop = offsetParent 的 padding-top + 中间元素的 offsetHeight + 当前元素的 margin-top

offsetLeft:相对版面或 offsetParent 属性指定父坐标的左部距离

  • offsetLeft = offsetParent 的 padding-left + 中间元素的 offsetWidth + 当前元素的 margin-left

Scroll

Scroll 指滚动。包括这个元素没有显示出来的实际宽度,包括 padding,不包括滚动条、border

scrollHeight:获取对象的滚动高度,对象的实际高度

scrollWidth:获取对象的滚动宽度

scrollTop:当前元素与窗口最顶端的距离

scrollLeft:当前元素与窗口最左端的距离

其他

innerHeight 和 clientHeight 有什么区别

准确来说,clientHeight 是针对 body,innerHeight 是 window 的

document.body.clientHeight:网页可见区域高

window.innerHeight:可视窗口高度,不包括浏览器顶部工具栏

监听图片高度实现懒加载

通过图片的 offsetTop(偏移高度)和 window 的 innerHeight、scrollTop 判断图片是否位于可视区域

即很多图片,先显示视窗中的图片,没看见的先不展示,加快页面加载速度。当你向下滚,当后续图片的 offsetTop(偏移高度) 小于 innerHeight(视窗高度) + scrollTop(滚动高度) 时,意味着此图片已经出现在视窗中,将真正图片替换loading

关键代码在于

function lazyload() {
  let seeHeight = window.innerHeight;
  let scrollTop = document.documentElement.scrollTop || document.body.scrollTop;
  for (let i = n; i < img.length; i++) {
    if (img[i].offsetTop < seeHeight + scrollTop) {
      // 对比图片的偏移高度和屏幕高度+滚动高度
      if (img[i].getAttribute("src") === "loading.gif") {
        img[i].src = img[i].getAttribute("data-src");
      }
      n = i + 1;
    }
  }
}

效果如下:

监听图片高度实现懒加载

Element.getBoundingClientRect

Element.getBoundingClientRect() 方法返回元素的大小及其相对于视口的位置

getBoundingClientRect 返回值是一个 DOMRect 对象,这个对象是由该元素的 getClientRects() 方法返回的一组矩形的集合,即与该元素相关的 CSS 边框集合

语法

domRect = element.getBoundingClientRect();

返回坐标、宽高、在视口中的位置

  • x
  • y
  • width
  • height
  • top
  • right
  • bottom
  • left

rect

如果是标准盒子模型,元素的尺寸等于 width/height + padding + border-width 的总和。如果 box-sizing:border-box ,元素的尺寸等于 width/height

我们用这个 API 来获取每张图片的 top 值,如果 top 值小于可视区的高度就视为已经进入可视区,直接加载图片即可

function lazyload() {
  let seeHeight = document.documentElement.clientHeight;
  for (let i = n; i < img.length; i++) {
    if (img[i].getBoundingClientRect().top < seeHeight) {
      if (img[i].getAttribute("src") === "loading.gif") {
        img[i].src = img[i].getAttribute("data-src");
      }
      n = i + 1;
    }
  }
}

效果如下:

getBoundingClientRect实现懒加载

通过 IntersectionObserver 实现懒加载

IntersectionObserver 接口(从属于 Intersection Observer API)提供了一种异步观察目标元素与其祖先元素或顶级文档视窗(viewport)交叉状态的方法。祖先元素与视窗(viewport)被称为根(root)

IntersectionObserver 可以不用监听 scroll 事件,做到元素一可见便调用回调,在回调里面我们来判断元素是否可见

if (IntersectionObserver) {
  let lazyImageObserver = new IntersectionObserver((entries, observer) => {
    entries.forEach((entry, index) => {
      let lazyImage = entry.target;
      // 如果元素可见
      if (entry.intersectionRatio > 0) {
        if (lazyImage.getAttribute("src") === "loading.gif") {
          lazyImage.src = lazyImage.getAttribute("data-src");
        }
        lazyImageObserver.unobserve(lazyImage);
      }
    });
  });
  for (let i = 0; i < img.length; i++) {
    lazyImageObserver.observe(img[i]);
  }
}

上述代码表示,遍历所有的图片,对其进行观察原生是否可见,如果元素可见,就把真正图片替换loading

IntersectionObserver 可以自动“观察”元素是否可见,其本质是目标元素与视窗产生一个交叉去,所以这个 API 叫做“交叉观察器”

使用方式

let io = new IntersectionObserver(callback, option);

上面代码中, IntersectionObserver 是浏览器原生提供的构造函数,接受两个参数:callback 是可见性变化时的回调函数,option 是配置对象(该参数可选)

构造函数的返回值是一个观察器实例。实例的 observe 方法可以指定观察哪个 DOM 节点

// 开始观察
io.observe(document.getElementById("example"));

// 停止观察
io.unobserve(element);

// 关闭观察器
io.disconnect();

callback 参数

目标元素的可见性变化时,就会调用观察器的回调函数 callback

callback 一般会触发两次。一次时目标元素刚刚进入视窗(开始可见),另一次时完全离开视窗(开始不可见)

let io = new IntersectionObserver((entries) => {
  console.log(entries);
});

上面代码中,回调函数采用的是箭头函数的写法。callback 函数的参数(entries)是一个数组,每个成员都是一个 IntersectionObserverEntry 对象。举例来说,如果同时有两个被观察的对象的可见性发生变化,entries数组就会有两个成员。

IntersectionObserverEntry 对象

IntersectionObserverEntry 对象提供目标元素的信息,一共有六个属性

  • time: 可见性发生变化的时间,单位毫秒
  • target:被观察的目标,是个 DOM 节点对象
  • rootBounds:根元素的矩形区域的信息,getBoundingClientRect() 方法的返回值,如果没有根元素(即直接相对于视窗滚动),则返回null
  • boundingClientRect:目标元素的矩形区域的信息
  • intersectionRect:目标元素与视窗(或根元素)的交叉区域的信息
  • intersectionRatio:目标元素的可见比例,即 intersectionRect 占 boundingClientRect 的比例,完全可见时为1,完全不可见时小于等于 0

如图所示:

IntersectionObserverEntry参数

兼容性如何

caniuse 兼容性报告目前支持率是 93.67%,但是iOS的支持度要在 iOS12.2 以上,如果是iPhoneX(2018.11)之后的手机都是支持的,如果是之前的,升级系统才支持,考虑到一些人是不会升级,所以这个兼容性还不支持大众化的场景,但它的能力和性能都非常的好

image-20220125104012150

总结

面试的时候被问到懒加载,我那个时候没做过相关的准备,我说不知道,途虎的面试官会引导,其实引导才能测试出一个人真正的水平,但是那个时候我竟然连 scroll 都想不起来。现在回想起来,实在是准备的方向搞错了。

说到图片懒加载,有两种方法:

  • 监听图片高度
    • 技术要点:监听scroll,滚动的时候遍历所有的图片,如果图片的偏移高度小于屏幕高度+滑动高度,说明已经出现在视窗,就替换图片
    • 优点:兼容性好
    • 缺点:单纯使用 scroll 滑动来监听高度,会引发性能问题,所以要搭配节流
  • Element.getBoundingClientRect
    • 技术要点:与监听图片无太大区别,无非视把图片的偏移高度改成 getBoundingClientRect().top,对比每张图片的自身高度是否出现在视窗(视口)中,有就替换图片
    • 优点:兼容性好,代码相对监听图片高度少了一些
    • 缺点:也是使用 scroll 滑动来监听,会引发性能问题
  • 使用 IntersectionObserver Api
    • 技术要点:通过 IntersectionObserver Api 来实现,图片元素一可见就调用回调,在回调中判断元素是否可见
    • 优点:写起来方便,性能好
    • 缺点:兼容性适配iOS12.2以上,安卓5以上

附上线上 demo:

参考资料

芝麻开门,显示全文!

部署实战记录

写博客是为了让自己不那么快忘记

把一个 node 服务部署上线是怎么个流程?

本人在做个人公众号微信分享服务时因为看到别人的个人订阅号能突破微信认证的界限,所以也想尝试一下,结果是失败了,但是又温习了下部署的整一流程,以此记录

现在开发通过

本地部署测试

上阿里云/腾讯云开启安全组规则,开放端口(我的服务是3003)

上服务器开放防火墙

用nginx 做域名映射

  • server {
        listen 80;
        server_name example.azhubaby.com;
        location / {
            proxy_set_header   X-Real-IP $remote_addr;
            proxy_set_header   Host      $http_host;
            proxy_pass         http://0.0.0.0:3003;
        }
    }
    

完毕

因为我的牵扯到微信JS-SDK,所以要去公众号配置JS安全域名

此后去微信开发者工具中测试

防火墙配置

测试命令:

telnet ip地址 端口

在本地window系统 cmd命令窗口输入该命令。ip地址为远程服务器的公网ip地址。

命令案例:

telnet 47.102.152.19 3003

查看防火墙状态

systemctl status firewalld.service

开启防火墙

systemctl start firewalld.service

关闭防火墙

systemctl stop firewalld.service

禁用防火墙

systemctl disable firewalld.service

查看防火墙已开放端口列表

firewall-cmd --list-all

防火墙添加端口

[root@localhost ~]# firewall-cmd --permanent --add-port=3003/tcp
success
[root@localhost ~]# firewall-cmd --reload
success
[root@localhost ~]# firewall-cmd --list-all
public (active)
  target: default
  icmp-block-inversion: no
  interfaces: ens33
  sources:
  services: dhcpv6-client ssh
  ports: 8080/tcp 3306/tcp
  protocols:
  masquerade: no
  forward-ports:
  source-ports:
  icmp-blocks:
  rich rules:

防火墙关闭端口

[root@localhost ~]# firewall-cmd --permanent --remove-port 3003/tcp
success
[root@localhost ~]# firewall-cmd --reload
success
[root@localhost ~]# firewall-cmd --list-all
public (active)
  target: default
  icmp-block-inversion: no
  interfaces: ens33
  sources:
  services: dhcpv6-client ssh
  ports: 8080/tcp
  protocols:
  masquerade: no
  forward-ports:
  source-ports:
  icmp-blocks:
  rich rules:

参考资料

芝麻开门,显示全文!

0.1+0.2!==0.3,为什么?

先说结论

为什么不等于?

因为浮点数表示小数的时候有精准度损失

为什么会有精准度损失

因为计算机硬件存储数据时,是以二进制(10101010)形式进行

比如说每个字节是 8 位,int 类型占 4 个字节,也就是 32 位精度;那么 32 位的计算机精度可以存 2 的 32次方个数据。如下图:

例子

每位上面可以放两个二进制数据也就是 0 或者 1;一般最高位上是符号位(1表示负数,0表示正数),所以带符号的类型数据应该是 31 个 2

2 _ 2 _ 2 _ … _ 2(31个2),加上符号范围就是 -2147483648 ~ 2147483647;当然也有无符号整形,暂不讨论

那么小数怎么存呢?小数在计算机当中叫浮点型,JS 最终会由浏览器引擎转成 C++,但是 JS 当中只有一种数值类型,那就是 number,那么 number 在 C++ 是什么类型呢;

我们暂且认为它是双精度类型,也就是 double,C++ 中占四个字节,也就是 64 位存储,整数存储参考上面即可,重点说说浮点存储

同样 64 位可分为三部分,它的制定格式是以 IEEE 754 为标准:

第一部分:符号位(S),占 1 位即第 63 位;

第二部分:指数域(E),占 11 位即 52 位到 62 位,含 52 和 62;

第三部分:尾数域(F),占 52 位即第 0 位到 51 位,含 51;

64-bit

如果将一个小数转换成二进制 64 位怎么表示,以 12.52571 为例

  • 先转换成二进制(十进制转换成二进制)(站长工具二进制转换
    • 12.52571 => 1100.100001101001010011101110001110010010111000011111
  • 将其小数点向左偏移三位
    • 1.100100001101001010011101110001110010010111000011111 * 2^3

得出结论

  1. 因为是整数,所以符号位 S 是 0;
  2. 因为向左偏移了三位,所以 E = 1023 + 3 = 1026(转化为二进制) => 10000000010,有 11 位,不够前面补 0
    • 为什么要加1023?为什么左移是加3,不是减3
  3. 尾数是(F)(小数点后面)100100001101001010011101110001110010010111000011111;

最终表示: 0 10000000010 100100001101001010011101110001110010010111000011111;

上面总长度是63位,差一位,最后面补零,即

0 10000000010 1001000011010010100111011100011100100101110000111110;

那么12.52571的64位计算机存储形式就是上面了;

回过头看 0.1 + 0.2

上面的表达可能有些疑惑,肯定的,毕竟笔者也是参考的(权当笔记,供以后温习),暂且不表;那么 0.1 和 0.2 是怎么转的

这里就有一个问题,0.1 和 0.2 转成二进制小数点后面是循环的

// 0.1 转化为二进制
0.0 0011 0011 0011 0011...(0011无限循环)
// 0.2 转化为二进制
0.0011 0011 0011 0011 0011...(0011无限循环)

由于尾数只有52位(52位之后的被计算机截掉了)

E = -4; F =1001100110011001100110011001100110011001100110011010 (52位)
E = -3; F =1.1001100110011001100110011001100110011001100110011010 (52位)

要让两个数相加,首先E需要相同,于是得出下面

E = -3; F =0.1100110011001100110011001100110011001100110011001101 (52位) //多余位截掉
E = -3; F =1.1001100110011001100110011001100110011001100110011010 (52位)

上面两个相加得出

E = -3; F = 10.0110011001100110011001100110011001100110011001100111
-------------------------------------------------------------------
E = -2; F = 1.00110011001100110011001100110011001100110011001100111

得出的结论就是

2 ^ (-2 * 1.00110011001100110011001100110011001100110011001100111);

这个值转换成真值,结果为: 0.30000000000000004

如何做到精准度

JavaScript 的类型 bigInt (ES8)中

TypeScript 也有这样的类型

有解决精准度问题的 big.js、bigInt 库

同样有精准度缺失的语言

python

总结

因为 JavaScript 到最后会转换为 C++ 去执行

在 IEEE754 标准中常见的浮点数数值表示有:单精准度(32位)和双精准度(64位),JS 采用的是后者。浮点数与整数不同,一个浮点数既包含整数部分,又包含小数部分,因为其表示法的不同,需要分析为整数和小数部分,然后相加得到结果。0.1 和 0.2 先转成二进制,在转换为同一维度计算,得到二进制后,再转换为十进制后,就成了0.30000000000000004

芝麻开门,显示全文!

移动端法门:自适应方案和高清方案

笔者从毕业开始做前端到现在,90% 的项目是移动端打交道,所以当简历上写了“移动H5”几个字时,必会被问到自适应方案与高清方案

”自适应“讲的是一套UI(例如750*1334),在多端下展示近乎一样的效果;而”高清“是因为 DPR 提升而所做的各种精度适配

这篇文章讲讲笔者理解的自适应方案和高清方案

先说结论

自适应方案

  • rem
    • 适配思路
      • 选择一个尺寸作为设计和开发基准
      • 定义一套适配规则,自动适配剩余的尺寸
      • 特殊适配效果给出设计效果
    • 属于历史产物,CSS 视窗单位未得到主流浏览器的支持
    • 原理
      • 根据视窗宽度动态调整根元素 html 的 font-size 的值
      • 把总宽度设置为 100 份,每一份被称为一个单位 x,同时设置 1rem 单位为 10x
    • 缺点
      • 需要加载 js 脚本,而且根据设备的视窗宽度进行计算,影响性能
    • 影响力:从2015年出世至今,在 H5 适配领域占据一定比例
    • 相关技术库:flexiblepx2rem
  • vw
    • 适配思路(如上)
    • 原理
      • 利用 CSS 视窗的特性,总宽度为 100vw,每一份为一个单位 1vw,设置 1rem 单位为 10vw
    • 缺点
      • 因为是根据视图的宽度计算,所以不适用平板和PC
    • 影响力:2018年出的方案,目前 H5 适配主流
    • 相关技术库:postcss-px-to-viewport
  • px + calc + clamp
    • 适配思路
      • 根据 CSS 的新特性:css变量、calc()函数、clamp()、@container函数实现
    • 特点
      • 解决了rem、vw布局的致命缺点:失去像素的完美性,而且一旦屏幕低于或高于某个阈值,通常就会出现布局的移动或文字内容的溢出
      • 大漠在2021年提出,最先进,但没看到大厂使用(clamp函数浏览器支持率暂且不高),具体可以看看大漠的这篇:如何构建一个完美缩放的UI界面
    • 缺点
      • 因为方案先进,暂没看到大厂使用

高清方案

  • 1 像素问题的解决方案
  • 不同 DPR 下图片的高清解决方案

综上,自适应方案是解决各终端的适配问题,高清方案是解决Retina屏的细节处理

写在前面

在说移动端适配方案之前先整明白一些技术概念

设备独立像素

设备独立像素(DIP)=== CSS 像素 === 逻辑像素,在 Chrome 中能直接看到 375* 667

chrome中查看css像素

当你看到设备独立像素时,不要慌,它表示 CSS 像素,而它的长宽就是在 Chrome 中所查到的。可这样记忆,“设备独立像素”,字数长,文绉绉就是 CSS 像素,也是理论上人为给定的指标,也叫逻辑像素

物理像素

物理像素可以理解为手机厂商在卖手机时宣传的分辨率,即物理像素 = 分辨率,它表示垂直和水平上所具有的像素点数

也就是说设备屏幕的水平方向上有 1920 像素点,垂直方向有 1080 像素点(假设屏幕分辨率为1920*1080),即屏幕分辨率表示物理像素,它在出厂时就定下来,单位为 pt,1pt=0.376mm

手机分辨率

物理像素,又被称为设备像素,即表示 设备像素 === 物理像素。可这样记忆,设备在物理世界能测量的长度

DPR(Device Pixel Ratio)

而设备像素比(DPR)是什么?

DPR = 设备像素 / 设备独立像素,它通常与视网膜屏(Retina 屏)有关

以 iPhone7 为例子,iPhone7 的 DPR = iPhone7 物理像素 / iPhone7 设备独立像素 = 2

宽 1334 / 667 = 2

高 750 / 375 = 2

得到 iPhone7 的 DPR 为 2,也就是我们常说的视网膜屏幕,而这就是营销术语,它就是因为技术的进步,使得一个 CSS 像素塞入更多的物理像素

营销术语还有哪些:农夫山泉的大自然的搬运工、元气森林的“気”

笔者是这么记忆的:

  • CSS 像素(设备独立像素)就像一个容器,以前是一比一塞入,所以 DPR 为 1,后来技术发展进步了,一个容器中能塞入更多的真实像素(物理像素)

  • DPR = 设备像素 / 设备独立像素

  • DPR = 物理像素(真实)/ CSS 像素(虚的)

在视网膜屏幕中,以 DPR = 2 为例,把 4(2x2)个物理像素当一个 CSS 像素使用,这样让屏幕看起来更加清晰(精致),但是元素的大小(CSS像素)本身不会改变

DPR对比

随着硬件的发展,像 iPhone13 Pro 等手机的 DPR 已经为 3,未来 DPR 突破 4 不是问题

说回来,DPR 为 2 或 3 会有什么问题?我们以 CSS 为最小单位来写代码的,展示在屏幕上也是以 CSS 为最小单位来展示,也就是说在 DPR 为 2 时,我们想要模拟 1 单位物理像素是做不到的(如果浏览器支持用 0.5px CSS 的话,可以模拟,但是DPR为 3 呢,用 0.333px?);又因为手机的设备独立像素(CSS 像素)固定,使用传统静态布局(固定 px)时,会出现样式的错位

iPhone 5/SE: 320 * 568 DPR: 2

iPhone 6/7/8: 375 * 667 DPR: 2

iPhone 6/7/8 Plus: 414* 736 DPR: 3

iPhone X: 375 * 812 DPR: 3

所以我们要适配各终端的 CSS 像素以及不同 DPR 下,出现的 1 像素问题、图片高清问题等。随着技术的发展,前端们摆脱了 IE 的兼容,同时陷入了各大手机品牌的兼容沼泽

自适应方案

Rem 布局——天下第二

简介:rem 就是相对于根元素 html 的 font-size 来做计算

与 rem 相关联的是 em:

em 作为 font-size 单位时,其代表父元素的字体大小,em 作为其它属性单位时,代表自身字体大小

rem 作用于非根元素时,相对于根元素字体大小,rem 作用于根元素字体时,相对于其初始字体大小

本质:等比缩放,是通过 JavaScript 来模拟 vw 的特性

假设将屏幕宽度平均分为 100 份,每一份的宽度用 x 表示,x = 屏幕宽度 / 100,如果将 x 作为单位,x 前面的数值就代表屏幕宽度的百分比

p {
  width: 50x;
} /* 屏幕宽度的 50% */

如果想要页面元素随着屏幕宽度等比变化,我们就需要上面的 x,这个 x 就是 vw,但是 vw 是在浏览器支持后才大规模使用,在此之前,js + rem 可模拟这种效果

之前说了,rem 作用于非根元素时,相对于根元素字体大小,所以我们设置根元素单位后,非根元素使用 rem 做相对单位

html {
  font-size: 16px;
}
p {
  width: 2rem;
} /* 32px */

html {
  font-size: 32px;
}
p {
  width: 2rem;
} /* 64px */

问题来了,我们要获取到一个动态的根元素 font-size,并以此变化各个元素大小

有趣的是,我司两个项目目前的做法是通过媒体查询设置根元素,分为四档,默认16px

笔者对这种做法表示不理解,原开发人员说我们这套运行了6年,UI适配也没人说什么问题。这里就有个疑问了,真的如他所说UI适配的很好吗,”媒体查询根元素+rem“也能适配好吗?是否完美呢?

后续笔者也会在 demo 中展示这种做法

但是根元素的 font-size 怎么变化,它不可能一直是 16px,在中大屏下还可以,但是在小屏下字体就太大了,所以它的大小也应该是动态获取的。如何让其动态化,就是上文所说,让根元素的 font-size 大小恒等于屏幕宽度的 1/100

html {
  font-size: width / 100;
}

如何设置 html 的字体大小恒等于屏幕宽度的百分之一呢?可以通过 js 来设置,一般需在页面 dom ready、resize 和屏幕旋转中设置

document.documentElement.style.fontSize =
  document.documentElement.clientWidth / 100 + "px";

flexible 源码就如以上思路写的

我们设置了百分之一的宽度后,在写 css 时,就需要利用 scss/less 等 css 处理器来对 css 编译处理。假设给出的设计图为 750 * 1334,其中一个元素宽度为 200 px,根据公式:

width: 200 / 750 * 100 = 26.67 rem

在 sass 中,需要设置设计图宽度来做换算:

@use "sass:math";

$width: 750px;

@function px2rem($px) {
  @return #{math.div($px, $width) * 100}rem;
}

上面编译完后

div {
  width: 26.667rem;
}

在不同尺寸下,它的宽度不同

机型尺寸width
iPhone 5/SE320 * 568170 * 170
iPhone 6/7/8375 * 667200 * 200
iPhone 6/7/8 Plus414 * 736220.797 * 220.797
iPhone X375 * 812200 * 200

效果如下(特意说明:图中演示的是引入 flexible 库,它的根元素的 font-size 为屏幕的 1/10)

rem布局

REM 布局(flexible)demo

优点:rem 的兼容性能低到 ios 4.1,android 2.1

缺点:

  • 等比放大(可以说优点也可以理解为缺点,不同场景下使用)

    • 用户选择大屏幕有几个出发点,有些人想要更大的字体,更大的图片,有些人想要更多的内容,并不想要更大的图标
  • 字体大小不能使用 rem(一般使用媒体查询控制 font-size 大小)

  • 在 PC 端浏览破相,一般设置一个最大宽度

var clientWidth = document.documentElement.clientWidth;
clientWidth = clientWidth < 780 ? clientWidth : 780;
document.documentElement.style.fontSize = clientWidth / 100 + "px";
body {
  margin: auto;
  width: 100rem;
}
  • 如果用户禁止 js 怎么办?

    • 添加 noscripe 标签提示用户

    • <noscript>开启JavaScript,获得更好的体验</noscript>

    • 给 HTML 添加一个 默认字体大小

相关技术方案:flexible(amfe-flexible 或者 lib-flexible) + postcss-pxtorem

Viewport 布局——天不生我VW,适配万古如长夜

vw 是基于 Viewport 视窗的长度单位,这里的视窗(Viewport) 指的是浏览器可视化的区域,而这个可视区域是 window.innerWidth/window.innerHeight 的大小

根据 CSS Values and Units Module Level 4: vw 等于初始包含块(html元素)宽度的1%,也就是

  • 1vw 等于 window.innerWidth 的数值的 1%
  • 1vh 等于 window.innerHeight 的数值的 1%

看图理解

屏幕的宽高

在说 rem 布局时,曾经举过 x 的例子,x 就是 vw

/* rem 方案 */
html {
  font-size: width / 100;
}
div {
  width: 26.67rem;
}

/* vw 方案 */
div {
  width: 26.67vw;
}

vw 还可以和 rem 方案结合,这样计算 html 字体大小就不需要 js 了

html {
  font-size: 1vw;
}
div {
  width: 26.67rem;
}

效果如下:

vw适配

vw 适配是 CSS 原生支持,而且目前兼容性大多数手机是支持的,也不需要加载 js ,也不会因为 js引发性能问题

vw 确实看上去很不错,但是也存在一些问题

  • 也没能很好的解决 1px 边框在高清屏下的显示问题,需要自行处理
  • 由于 vw 方案是完全的等比缩放,在 PC 端上会破相(和 rem一样)

相关技术方案:postcss-px-to-viewport

VW 布局demo

px适配——一力降十会

不用 rem/vw,用传统的响应式布局也能在移动端布局中使用,需要设计规范

使用css 变量适配(篇幅原因暂不详细介绍,可直接看代码

使用场景:新闻、内容型的网站,不太适用 rem,因为大屏用户想要看到更多的内容,如网易新闻、知乎、taptap

PX + CSS变量 demo

媒体查询——可有我一席?

上文讲到我司原先H5端采用媒体查询的方式来做适配,笔者尝试复刻了下,只能说大差不差,能看出媒体查询想做成这件事,但还是心有余而力不足

采用rem、vw、px等方法能实现非标准尺寸(375 _ 667设计稿)下 header 的高度为 165.59px,而 media 因为大屏,将根font-size 设置为17px,结果 header 的高度成为 159.38px(17 _ 9.375rem)

如下GIF所示:

媒体查询布局与其他布局对比

所以说仅用媒体查询还是差强人意

媒体查询布局demo

各种适配的对比

vw、rem 适配的本质都是等比例缩放,px 直接写,孰优孰劣看自己

REM布局VW布局PX + css变量布局
容器最小宽度支持不支持支持
容器最大宽度支持不支持支持
高清设备1px边框支持支持支持
容器固定纵横比支持支持支持
优点1.老牌方案
2.支持高清设备1px边框时,可按以往方式直接写
1.无需引入js
2. 天然支持,写法规范
同VW
缺点1. 需要引入 js 设置 html 的font-size
2. 字体大小不能使用 rem
3. 在 PC 端浏览会破相,一般需设置最大宽度
1.在PC端会破相
2.不支持老旧手机
同VW

除此之外,还有搭配 vw 和rem 的方案

  • 给根元素大小设置随着视窗变化而变化的vw单位,动态变化各元素大小
  • 限制根元素字体大小的最大最小值,配合body加上最大宽度和最小宽度
// rem 单位换算:定为 75px 只是方便运算,750px-75px、640-64px、1080px-108px,如此类推
$vm_fontsize: 75; // iPhone 6尺寸的根元素大小基准值
@function rem($px) {
  @return ($px / $vm_fontsize) * 1rem;
}
// 根元素大小使用 vw 单位
$vm_design: 750;
html {
  font-size: ($vm_fontsize / ($vm_design / 2)) * 100vw;
  // 同时,通过Media Queries 限制根元素最大最小值
  @media screen and (max-width: 320px) {
    font-size: 64px;
  }
  @media screen and (min-width: 540px) {
    font-size: 108px;
  }
}
// body 也增加最大最小宽度限制,避免默认100%宽度的 block 元素跟随 body 而过大过小
body {
  max-width: 540px;
  min-width: 320px;
}

高清方案

1像素问题

1像素指在 Retina 屏显示 1单位物理像素

很好理解,CSS 像素(设备独立像素)是我们人为规定的,当 DPR 为 1 时,1像素(指我们写的 CSS 像素) 等于 1物理像素;但当 DPR 为 3 时,1像素就为 3 物理像素

  • DPR = 1,此时 1 物理像素 等于 1 CSS 像素
  • DPR = 2,此时 1 物理像素等于 0.5 CSS 像素
    • border-width: 1px,这里的 1px 其实是 1 CSS 像素宽度,等于 2 物理像素,设计师其实想要的是 border-width: 0.5px
  • DPR = 3,此时 1 物理像素等于 0.33 CSS 像素
    • 设计师想要的是 border-width: 0.33px

1像素问题

解决思路

使用 0.5px 。有局限性,iOS 8及以上,苹果系统支持,但是 iOS 8以下和 Android(部分低端机),会将0.5px 显示为 0px

既然 1 个 CSS 像素代表 2(DPR 为2)、3(DPR为3)物理像素,设备又不认识 0.5px 的写法,那就画 1px,然后想办法将宽度减少一半

方案

  • 渐变实现
    • background-image: linear-gradient(to top, ,,,)
  • 使用缩放实现
    • transform: scaleY(0.333)
  • 使用图片实现
    • base64
  • 使用 SVG 实现
    • 嵌入 background url
  • border-image
    • 低端机下支持度不好

以上都是通过 CSS 的媒体查询来实现的

@media only screen and (-webkit-min-device-pixel-ratio: 2),
  only screen and (min-device-pixel-ratio: 2) {
}
@media only screen and (-webkit-min-device-pixel-ratio: 3),
  only screen and (min-device-pixel-ratio: 3) {
}

图片适配和优化

图像通常占据了网页上下载资源绝大部分,优化图像通常可以最大限度地减少从网站下载的字节数以及提高网站性能

通常可以,有一些通用的优化手段:为不同 DPR 屏幕提供最适合的图片尺寸

各大厂商的适配分析

看了不少文章,类似如:大厂是怎么做移动端适配的

各大厂,有用rem适配的、也有用vm适配的、也有vm+rem结合适配的,纯用 px 方案的也有

  • 新闻、社区等可阅读内容较多的场景:px+flex+百分比
    • 如携程、知乎、TapTap
  • 对视觉组件种类较多,依赖性较强的移动端页面:vw+rem
    • 如电商、论坛

总结

rem 方案,引入 amfe-flexible

设计:设计出图是 750 * 1334,设计切好图后,上传蓝湖,按照尺寸写 px。

开发:

  • 使用 rem 方案
    • 引入 amfe-flexible
    • 安装 px2rem 之类的 px 转 rem 工具
    • 配置 px2rem
    • 在项目中写 px ,输出时是 rem
    • 适用任何场景
  • 使用 vw 方案
    • 安装 px2vw 之类的 px 转 vw 工具
    • 配置 px2vw
    • 在项目中写 px,输出时是 vw
    • 适用任何场景
  • 使用 px 方案
    • 该怎么样就怎么写,不过因为有设计规划,按钮的大中小尺寸固定、icon 的尺寸有标准、TabBar 的高度也是写死的,当一切都有标准后,写页面就方便了
    • 例如
      • 左边固定 100 * 50,右边 flex 布局
      • 左边固定 100 * 50,右边 calc(100% - 100px)(使用 CSS3 中的 calc 计算)

其他

caniuse 网站测试CSS属性与浏览器的兼容性问题

疑问

Q:为什么 H5 移动端UI库单位大都是用 px?这样不会有适配问题吗?

其实我们写好 px 后,如果项目采用 rem 写业务,引入 px2rem(已经六年没有维护了) 即可转换。

在有赞 vant 库中,它对浏览器适配的介绍是:

Viewport 布局

Vant 默认使用 px 作为样式单位,如果需要使用 viewport 单位(vw、vh、vmin、vmax),推荐使用 postcss-px-to-viewport 进行转换

postcss-px-to-viewport 是一款 PostCSS 插件,用于将 px 单位转化为 vw/vh 单位

Rem 布局

如果需要使用 rem 单位进行适配,推荐使用以下两个工具:

  • postcss-pxtorem 是一款 PostCSS 插件,用于将 px 单位转化为 rem 单位
  • lib-flexible 用于设置 rem 基准值

demo 合集:线上demo

参考资料

芝麻开门,显示全文!

搞轮子:SortBar排序栏的升级

前言

在此之前,曾经写过两篇关于组件嵌套关系的组件文章:

搞轮子:从业务到组件Tabs

搞轮子:TabBar标签栏的自救

Tabs 组件首次使用 React.Children.map 来”解构“子组件,释放子组件写法

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

TabBar 组件再次基础上,又使用了 React.cloneElement,使组件的状态变化由父组件提供

<TabBar
  activeKey={activeKey}
  onChange={(key: any) => {
    setActiveKey(key);
  }}
>
  <TabBar.Item itemKey="home" title="主页" icon={<IconTabbarHome />} />
  <TabBar.Item
    itemKey="financial"
    title="理财"
    icon={<IconTabbarFinancial />}
  />
  <TabBar.Item itemKey="user" title="我的" icon={<IconTabbarUser />} />
</TabBar>

TabBar 是底部的菜单栏,有选中和非选择两种状态,所以要给父组件设置”当前选中项“,每个子组件设置唯一标识符,这样才能判断那个元素被选中了

而今天说的 SortBar 状态变为三种,分别为未选择,降序、升序

正文

我们要做成什么样子呢?如下图所示:

image-20211223091530386

有筛选、和三个有状态的子组件。其实和 TabBar 组件很像,无非是多了筛选组件把两种状态改为三种

先看书写结构:

<SortBar
  activeKey={activeKey}
  onChange={(key: string, status: string) => {
    setActiveKey(key);
  }}
  onClick={() => {
    console.log("点击筛选");
  }}
>
  <SortBar.Item title="年化" itemKey="annualized" />
  <SortBar.Item title="期限" itemKey="term" />
  <SortBar.Item title="价格" itemKey="price" />
</SortBar>

和 TabBar 可以说一摸一样,其中 onClick 是针对点击筛选组件的,其核心思路和 TabBar一样,核心代码是:

const items = React.Children.map(children, (element, index) => {
  if (!React.isValidElement(element)) return null;

  return cloneElement(element, {
    key: index,
    title: element.props.title,
    itemKey: element.props.itemKey || index,
    selected: activeKey === element.props.itemKey,
    onClick: (status: string) => onHandleClick(element.props.itemKey, status),
  });
});

React.Children 遍历子元素

React.isValidElement 判断子元素是否是 React 元素

cloneElement 在原来的子组件上添加其他元素

  • selected:新添加的元素,判断当前选中key和自身的 key 是否是同一个
  • onClick:点击的时候哪个key,什么status(状态)传给父元素,好让父元素做业务处理

选中和未选中此子组件由父组件控制,而选中此子组件的状态则在子组件中完成,请看代码:

const Item: FC<SortBarItemProps> = (props) => {
  const { title, selected, onClick } = props;
  const [status, setStatus] = useState("0");

  useEffect(() => {
    if (selected === false) {
      setStatus("0");
    }
  }, [selected]);

  const onHandleClick = () => {
    if (status === "0" || status === "2") {
      setStatus("1");
      onClick && onClick("1");
    } else if (status === "1") {
      setStatus("2");
      onClick && onClick("2");
    }
  };

  return (
    <div className={`${prefixCls}`} onClick={onHandleClick}>
      {title}
      {status === "0" && <IconFilterEmty size="sm" />}
      {status === "1" && <IconFilterDown size="sm" />}
      {status === "2" && <IconFilterUp size="sm" />}
    </div>
  );
};

默认为都未选择,当点击后改变状态。

总结

这一些告一段落,已经没什么好讲的了

芝麻开门,显示全文!

知识点:preventDefault、stopPropagation

JavaScript 冒泡和捕获是两种事件行为,使用 event.stopPropagation() 能起到阻止捕获和冒泡阶段中当前事件的进一步传播,使用 event.preventDefault() 可以取消默认事件

防止冒泡和捕获

w3c 的方法是 e.stopPropagation(),IE则是使用 e.cancelBubble = true

取消默认事件

w3c 的方法是 e.preventDefault(),IE则是使用 e.returnValue = false

preventDefault 及 stopPropagation

preventDefault 阻止元素的默认特性

stopPropagation 禁止冒泡

event.stopPropagation 阻止捕获和冒泡阶段

event.preventDefault 取消默认事件

http://caibaojian.com/javascript-stoppropagation-preventdefault.html

https://segmentfault.com/a/1190000008227026

Unable to preventDefault inside passive event listener invocation

addEventListener 不为人知的第三个参数 useCapture

https://juejin.cn/post/6844903593024159752

[筆記][JavaScript]所謂的「停止事件」到底是怎麼一回事?

https://ithelp.ithome.com.tw/articles/10198999

https://developer.mozilla.org/zh-CN/docs/Web/API/EventTarget/addEventListener#%E4%BD%BF%E7%94%A8_passive_%E6%94%B9%E5%96%84%E7%9A%84%E6%BB%9A%E5%B1%8F%E6%80%A7%E8%83%BD

在 react 如何解决

react 的事件是自己写的

在 组件中如何解决

{passive: true} 有什么用

如何查看web元素绑定的监听事件

https://cloud.tencent.com/developer/article/1705416

芝麻开门,显示全文!

搞轮子:不依赖外部DIY Toast 需要考虑什么

mask(遮罩)

import React, { FC, memo } from "react";
import classnames from "classnames";
import { MaskProps } from "./PropType";

const prefixCls = "jing-mask";

const Mask: FC<MaskProps> = (props) => {
  const { className, type, visible, style, onClick } = props;

  const classes = classnames(className, prefixCls, {
    [`${prefixCls}--${type}`]: !!type,
  });

  return visible ? (
    <div className={classes} style={style} onClick={onClick} />
  ) : null;
};

Mask.defaultProps = {
  type: "normal",
  visible: false,
};

export default memo(Mask);

缺点在于它

前提

animation(transition) 组件

mask(遮罩)组件(搞定)

portal 组件

最简单版本的

import React, { FC, useState, useEffect, memo } from "react";
import { createPortal } from "react-dom";
import { PortalProps } from "./PropType";

const Portal: FC<PortalProps> = (props) => {
  const { children, container, className } = props;

  const [containerEl] = useState(() => {
    const el = document.createElement("div");
    el.className += `jing-portal__container ${className}`;
    return el;
  });

  useEffect(() => {
    document.body.appendChild(container || containerEl);

    return () => {
      document.body.removeChild(containerEl);
    };
  }, []);

  return createPortal(children, containerEl);
};

export default memo(Portal);

定位 popup,弹出层

popup 组件

使用方式

当作静态函数使用

Toast(…)

Toast.loading()

组件本身要考虑什么功能

遮罩(mask)、内容(message)、是否禁止背景点击(forbidClick),支持自定义图标(icon)、自定义出现的位置(position)、是否在点击遮罩层后关闭(closeOnClickOverlay)

展示时长(duration)、关闭时的回调函数(onClose)

提供 Toast.success 表示成功,

Toast.fail 表示失败

Toast.loading 表示加载

单例模式

allowMultiple

梳理一下

Toast的基础是 Popup,Popup的基础是 Mask 和 Portal

Popup要做的就是弹出层,属于基础组件

Modal 要做之前 popup.alert 之类的事情(Vant 是 Dialog)

Toast 要实例化

无论是 Toast 还是 Modal 都需要使用静态方法调用

Modal 可以大写

popup 和 portal 放一起

不可见的时候看不到元素,

可见的时候渲染元素

动画

react-transition-group 和 portal 的结合

因为 portal 是return 出的组件,所以不会有动画

需要做 animation

https://stackoverflow.com/questions/54672784/animating-react-transition-group-with-reactdom-createportal

有人说给他加 animationDuration

https://codesandbox.io/s/stupefied-bouman-ehszt?file=/src/components/Portal/index.js:517-526

一定会有 div

节点从有到无

image-20211215181234263

animatedVisible 成为 true,显示Portal 组件,先渲染父组件,再渲染子组件

先执行

破除魔咒,取消

useEffect(() => {
  mountContainer?.appendChild(containerEl);

  return () => {
    mountContainer?.removeChild(containerEl);
  };
}, []);

react-transition-group 的用法

http://reactcommunity.org/react-transition-group/transition#Transition-prop-onExited

Toast 不是一个组件,而是一个 ”方法“, Toast("提示内容")

怎么把一个 <Toast>提示文字<Toast> 写法的组件改造成 Toast("提示内容")

查看了别人做的 Toast,

先做个 React 版本的 Toast,即组件时写法,然后再将它作为基础组件使用,怎么使用,

生成一个 div(document.createElement(‘div’))

插入 dom 中(bodyContainer.appendChild(container))

ReactDOM.render() 渲染它

我们先完成 Toast 组件

Toast.loading()

需要完成 loading 组件

loading 之后

要 useloading

show

hide

目的是把它当作一个静态函数使用

Loading.

const loading = Loading.useLoading();

<Button
  size="xs"
  onClick={() => {
    loading.show();
    setTimeout(() => {
      loading.hide();
    }, 3000);
  }}
>
  开启
</Button>

这里有个思考角度,loading 需不需要被 popup 包裹,我的想法是按照实际需求来做组件,我们可以做成像弹出层那样,但是 loading 的用法,一般是作为 Toast 的子组件来使用,所以这里,useLoading 对我们不适用

当然,做这个是为了铺垫 useToast,它也需要具备 Toast.show() 的用法

show 的时候 ReactDOM.render 过程

芝麻开门,显示全文!